Move SSD group focus math to typed Kotlin

ober

d4ac6c1b5b9b6523090bb4d1662335f39e50e4c3

diff --git a/templates/ssd-review.ss b/templates/ssd-review.ss
index 7d7c62a..b43ea43 100644
--- a/templates/ssd-review.ss
+++ b/templates/ssd-review.ss
@@ -1138,7 +1138,8 @@
       (typed-library (com sfb ssdreview)
         (export hitGroupId selectionMatchingCells cellIdsForSelection
                 clampInitialScale clampScale clampGroupFitScale
-                clampViewportOffset clampImageCoordinate)
+                clampViewportOffset clampImageCoordinate
+                focusGroupScale groupCenterPoint)
         (type Float32)
         (type FloatArray)
         (def (selectionCellMatches? (rect : FloatArray) (cell : SsdCell)) : Bool
@@ -1185,6 +1186,43 @@
         (def (clampImageCoordinate (value : Float32)
                                    (maximum : Float32)) : Float32
           (clampFloat32 value (float32 0.0) maximum))
+        (def (maxFloat32Value (a : Float32) (b : Float32)) : Float32
+          (if (> a b) a b))
+        (def (minFloat32Value (a : Float32) (b : Float32)) : Float32
+          (if (< a b) a b))
+        (def (groupCenterPoint (group : SsdGroup)) : FloatArray
+          (let ((bbox (SsdGroup-bbox group)))
+            (float-array
+              (/ (+ (float-array-ref bbox (int32 0))
+                    (float-array-ref bbox (int32 2)))
+                 (float32 2.0))
+              (/ (+ (float-array-ref bbox (int32 1))
+                    (float-array-ref bbox (int32 3)))
+                 (float32 2.0)))))
+        (def (focusGroupScale (currentScale : Float32)
+                              (viewportWidth : Float32)
+                              (viewportHeight : Float32)
+                              (group : SsdGroup)) : Float32
+          (let ((bbox (SsdGroup-bbox group))
+                (padding (float32 2.4)))
+            (let ((groupWidth
+                    (maxFloat32Value
+                      (float32 16.0)
+                      (- (float-array-ref bbox (int32 2))
+                         (float-array-ref bbox (int32 0)))))
+                  (groupHeight
+                    (maxFloat32Value
+                      (float32 16.0)
+                      (- (float-array-ref bbox (int32 3))
+                         (float-array-ref bbox (int32 1))))))
+              (let ((fitScale
+                      (minFloat32Value
+                        (/ viewportWidth (* groupWidth padding))
+                        (/ viewportHeight (* groupHeight padding)))))
+                (clampScale
+                  (maxFloat32Value
+                    currentScale
+                    (clampGroupFitScale fitScale)))))))
         (def (pointInPaddedGroup? (group : SsdGroup)
                                   (x : Float32)
                                   (y : Float32)
@@ -6666,12 +6704,10 @@
 	       ""
 	       "    fun centerOnGroup(group: SsdGroup) {"
 	       "        if (bitmap == null || width <= 0 || height <= 0) return"
-	       "        val groupWidth = max(16f, group.bbox[2] - group.bbox[0])"
-	       "        val groupHeight = max(16f, group.bbox[3] - group.bbox[1])"
-	       "        val fitScale = min(width.toFloat() / (groupWidth * 2.4f), height.toFloat() / (groupHeight * 2.4f))"
-	       "        scale = clampScale(max(scale, clampGroupFitScale(fitScale)))"
+	       "        scale = focusGroupScale(scale, width.toFloat(), height.toFloat(), group)"
 	       "        updateMatrix()"
-	       "        val center = imageToView((group.bbox[0] + group.bbox[2]) / 2f, (group.bbox[1] + group.bbox[3]) / 2f)"
+	       "        val groupCenter = groupCenterPoint(group)"
+	       "        val center = imageToView(groupCenter[0], groupCenter[1])"
 	       "        offsetX += width / 2f - center[0]"
 	       "        offsetY += height / 2f - center[1]"
 	       "        clampOffsets()"