Move SSD view clamp math to typed Kotlin

ober

ce51755f0603742c440441897c9d4786dd76310b

diff --git a/templates/ssd-review.ss b/templates/ssd-review.ss
index df336fa..7d7c62a 100644
--- a/templates/ssd-review.ss
+++ b/templates/ssd-review.ss
@@ -1136,7 +1136,9 @@
 
     (typed-kotlin-file "com/sfb/ssdreview/SsdReviewGeometry.kt"
       (typed-library (com sfb ssdreview)
-        (export hitGroupId selectionMatchingCells cellIdsForSelection)
+        (export hitGroupId selectionMatchingCells cellIdsForSelection
+                clampInitialScale clampScale clampGroupFitScale
+                clampViewportOffset clampImageCoordinate)
         (type Float32)
         (type FloatArray)
         (def (selectionCellMatches? (rect : FloatArray) (cell : SsdCell)) : Bool
@@ -1160,6 +1162,29 @@
             (begin
               (mutable-list-add! ids (SsdCell-id (list-ref cells i)))
               ids)))
+        (def (clampFloat32 (value : Float32)
+                           (minimum : Float32)
+                           (maximum : Float32)) : Float32
+          (if (< value minimum)
+            minimum
+            (if (> value maximum)
+              maximum
+              value)))
+        (def (clampInitialScale (value : Float32)) : Float32
+          (clampFloat32 value (float32 0.1) (float32 8.0)))
+        (def (clampScale (value : Float32)) : Float32
+          (clampFloat32 value (float32 0.2) (float32 8.0)))
+        (def (clampGroupFitScale (value : Float32)) : Float32
+          (clampFloat32 value (float32 1.4) (float32 6.5)))
+        (def (clampViewportOffset (offset : Float32)
+                                  (viewportSize : Float32)
+                                  (scaledSize : Float32)) : Float32
+          (if (<= scaledSize viewportSize)
+            (/ (- viewportSize scaledSize) (float32 2.0))
+            (clampFloat32 offset (- viewportSize scaledSize) (float32 0.0))))
+        (def (clampImageCoordinate (value : Float32)
+                                   (maximum : Float32)) : Float32
+          (clampFloat32 value (float32 0.0) maximum))
         (def (pointInPaddedGroup? (group : SsdGroup)
                                   (x : Float32)
                                   (y : Float32)
@@ -6607,8 +6632,7 @@
        "        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 = max(width.toFloat() / displayWidth.toFloat(), height.toFloat() / displayHeight.toFloat())"
-       "            .coerceIn(0.1f, 8f)"
+       "        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"
@@ -6645,7 +6669,7 @@
 	       "        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 = max(scale, fitScale.coerceIn(1.4f, 6.5f)).coerceIn(0.2f, 8f)"
+	       "        scale = clampScale(max(scale, clampGroupFitScale(fitScale)))"
 	       "        updateMatrix()"
 	       "        val center = imageToView((group.bbox[0] + group.bbox[2]) / 2f, (group.bbox[1] + group.bbox[3]) / 2f)"
 	       "        offsetX += width / 2f - center[0]"
@@ -6842,7 +6866,7 @@
 	       ""
 	       "    private fun zoomAt(viewX: Float, viewY: Float, factor: Float) {"
 	       "        val before = viewToImage(viewX, viewY)"
-	       "        scale = (scale * factor).coerceIn(0.2f, 8f)"
+	       "        scale = clampScale(scale * factor)"
 	       "        updateMatrix()"
 	       "        val after = imageToView(before[0], before[1])"
 	       "        offsetX += viewX - after[0]"
@@ -6859,16 +6883,8 @@
 	       "        val displayHeight = if (rotationDegrees % 180 == 0) img.height.toFloat() else img.width.toFloat()"
 	       "        val scaledWidth = displayWidth * scale"
 	       "        val scaledHeight = displayHeight * scale"
-	       "        offsetX = if (scaledWidth <= width) {"
-	       "            (width - scaledWidth) / 2f"
-	       "        } else {"
-	       "            offsetX.coerceIn(width - scaledWidth, 0f)"
-	       "        }"
-	       "        offsetY = if (scaledHeight <= height) {"
-	       "            (height - scaledHeight) / 2f"
-	       "        } else {"
-	       "            offsetY.coerceIn(height - scaledHeight, 0f)"
-	       "        }"
+	       "        offsetX = clampViewportOffset(offsetX, width.toFloat(), scaledWidth)"
+	       "        offsetY = clampViewportOffset(offsetY, height.toFloat(), scaledHeight)"
 	       "    }"
        ""
        "    private fun viewToImage(x: Float, y: Float): FloatArray {"
@@ -6876,8 +6892,8 @@
        "        inverse.mapPoints(point)"
        "        val img = bitmap"
        "        if (img != null) {"
-       "            point[0] = point[0].coerceIn(0f, img.width.toFloat())"
-       "            point[1] = point[1].coerceIn(0f, img.height.toFloat())"
+       "            point[0] = clampImageCoordinate(point[0], img.width.toFloat())"
+       "            point[1] = clampImageCoordinate(point[1], img.height.toFloat())"
        "        }"
 	       "        return point"
 	       "    }"