Move SSD image fit math to typed Kotlin

ober

4723d608e185bca1a2a8ca6c2f921d9ded9a4444

diff --git a/templates/ssd-review.ss b/templates/ssd-review.ss
index b43ea43..bde3869 100644
--- a/templates/ssd-review.ss
+++ b/templates/ssd-review.ss
@@ -1139,7 +1139,8 @@
         (export hitGroupId selectionMatchingCells cellIdsForSelection
                 clampInitialScale clampScale clampGroupFitScale
                 clampViewportOffset clampImageCoordinate
-                focusGroupScale groupCenterPoint)
+                focusGroupScale groupCenterPoint
+                fitImageScale fitImageOffsetX fitImageOffsetY)
         (type Float32)
         (type FloatArray)
         (def (selectionCellMatches? (rect : FloatArray) (cell : SsdCell)) : Bool
@@ -1186,6 +1187,22 @@
         (def (clampImageCoordinate (value : Float32)
                                    (maximum : Float32)) : Float32
           (clampFloat32 value (float32 0.0) maximum))
+        (def (fitImageScale (viewportWidth : Float32)
+                            (viewportHeight : Float32)
+                            (displayWidth : Float32)
+                            (displayHeight : Float32)) : Float32
+          (clampInitialScale
+            (maxFloat32Value
+              (/ viewportWidth displayWidth)
+              (/ viewportHeight displayHeight))))
+        (def (fitImageOffsetX (viewportWidth : Float32)
+                              (scaledWidth : Float32)) : Float32
+          (if (> scaledWidth viewportWidth)
+            (- viewportWidth scaledWidth)
+            (/ (- viewportWidth scaledWidth) (float32 2.0))))
+        (def (fitImageOffsetY (viewportHeight : Float32)
+                              (scaledHeight : Float32)) : Float32
+          (/ (- viewportHeight scaledHeight) (float32 2.0)))
         (def (maxFloat32Value (a : Float32) (b : Float32)) : Float32
           (if (> a b) a b))
         (def (minFloat32Value (a : Float32) (b : Float32)) : Float32
@@ -6670,10 +6687,11 @@
        "        if (width <= 0 || height <= 0) return"
        "        val displayWidth = if (rotationDegrees % 180 == 0) img.width else img.height"
        "        val displayHeight = if (rotationDegrees % 180 == 0) img.height else img.width"
-       "        scale = clampInitialScale(max(width.toFloat() / displayWidth.toFloat(), height.toFloat() / displayHeight.toFloat()))"
-       "        val scaledWidth = displayWidth * scale"
-       "        offsetX = if (scaledWidth > width) width - scaledWidth else (width - scaledWidth) / 2f"
-	       "        offsetY = (height - displayHeight * scale) / 2f"
+       "        scale = fitImageScale(width.toFloat(), height.toFloat(), displayWidth.toFloat(), displayHeight.toFloat())"
+       "        val scaledWidth = displayWidth.toFloat() * scale"
+       "        val scaledHeight = displayHeight.toFloat() * scale"
+       "        offsetX = fitImageOffsetX(width.toFloat(), scaledWidth)"
+	       "        offsetY = fitImageOffsetY(height.toFloat(), scaledHeight)"
 	       "        clampOffsets()"
 	       "        updateMatrix()"
 	       "        invalidate()"