Move SSD review fit image math to typed Kotlin

ober

a8c905c65acb1bd1e592ee5a88419d8ee947536b

diff --git a/templates/ssd-review.ss b/templates/ssd-review.ss
index b1d14b2..5596cc6 100644
--- a/templates/ssd-review.ss
+++ b/templates/ssd-review.ss
@@ -1240,7 +1240,7 @@
                 selectionMatchingCells cellIdsForSelection
                 clampInitialScale clampScale clampGroupFitScale
                 clampViewportOffset clampImageCoordinate
-                focusGroupScale groupCenterPoint
+                focusGroupScale groupCenterPoint fitImageViewportState
                 fitImageScale fitImageOffsetX fitImageOffsetY
                 rotatedDisplayWidth rotatedDisplayHeight
                 rotatedDisplayWidthFloat rotatedDisplayHeightFloat
@@ -1492,6 +1492,42 @@
                                         (imageWidth : Int32)
                                         (imageHeight : Int32)) : Float32
           (float32 (rotatedDisplayHeight rotationDegrees imageWidth imageHeight)))
+        (def (fitImageViewportState (bitmap : (Nullable Bitmap))
+                                    (viewportWidth : Int32)
+                                    (viewportHeight : Int32)
+                                    (rotationDegrees : Int32)) : (Nullable ViewportState)
+          (if (or (nullable-null? bitmap)
+                  (viewDimensionsMissing viewportWidth viewportHeight))
+            (nullable-none ViewportState)
+            (let ((img (nullable-get bitmap)))
+              (let ((imageWidth (reviewBitmapWidth img))
+                    (imageHeight (reviewBitmapHeight img)))
+                (let ((displayWidth
+                        (rotatedDisplayWidthFloat
+                          rotationDegrees
+                          imageWidth
+                          imageHeight))
+                      (displayHeight
+                        (rotatedDisplayHeightFloat
+                          rotationDegrees
+                          imageWidth
+                          imageHeight))
+                      (viewportWidthF (float32 viewportWidth))
+                      (viewportHeightF (float32 viewportHeight)))
+                  (let ((nextScale
+                          (fitImageScale
+                            viewportWidthF
+                            viewportHeightF
+                            displayWidth
+                            displayHeight)))
+                    (nullable-some
+                      (make-ViewportState
+                        nextScale
+                        (fitImageOffsetX viewportWidthF (* displayWidth nextScale))
+                        (fitImageOffsetY viewportHeightF (* displayHeight nextScale))
+                        rotationDegrees
+                        imageWidth
+                        imageHeight))))))))
         (def (viewportCenterOffsetDelta (viewportSize : Int32)
                                         (center : Float32)) : Float32
           (- (/ (float32 viewportSize) (float32 2.0)) center))
@@ -8460,16 +8496,12 @@
        "    }"
        ""
        "    fun fitImage() {"
-       "        if (!bitmapLoaded(bitmap)) return"
-       "        val img = checkNotNull(bitmap)"
-       "        if (viewDimensionsMissing(width, height)) return"
-       "        val displayWidth = rotatedDisplayWidth(rotationDegrees, img.width, img.height)"
-       "        val displayHeight = rotatedDisplayHeight(rotationDegrees, img.width, img.height)"
-       "        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)"
+       "        val fitViewport = fitImageViewportState(bitmap, width, height, rotationDegrees)"
+       "        if (!viewportStatePresent(fitViewport)) return"
+       "        val savedViewport = viewportStateOrEmpty(fitViewport)"
+       "        scale = savedViewport.scale"
+       "        offsetX = savedViewport.offsetX"
+	       "        offsetY = savedViewport.offsetY"
 	       "        clampOffsets()"
 	       "        updateMatrix()"
 	       "        invalidate()"