Preserve manual SSD group orientation

ober

6944652a0a194ef699558bc010d91cd046c2fd77

diff --git a/templates/ssd-review.ss b/templates/ssd-review.ss
index 91b0172..aa18857 100644
--- a/templates/ssd-review.ss
+++ b/templates/ssd-review.ss
@@ -338,7 +338,6 @@
        "import kotlin.math.ceil"
        "import kotlin.math.max"
        "import kotlin.math.min"
-       "import kotlin.math.sqrt"
        ""
        "class MainActivity : Activity() {"
        "    private lateinit var truthStore: TruthStore"
@@ -785,6 +784,9 @@
        "            .put(\"viewport_offset_x\", reviewView.captureViewport()?.offsetX ?: 0f)"
        "            .put(\"viewport_offset_y\", reviewView.captureViewport()?.offsetY ?: 0f)"
        "            .put(\"viewport_rotation\", reviewView.captureViewport()?.rotationDegrees ?: 0)"
+       "            .put(\"manual_grid_horizontal\", manualCellGridDimensions(floatArrayOf(0f, 0f, 160f, 40f), 4).let { \"${it.first}x${it.second}\" })"
+       "            .put(\"manual_grid_vertical\", manualCellGridDimensions(floatArrayOf(0f, 0f, 40f, 160f), 4).let { \"${it.first}x${it.second}\" })"
+       "            .put(\"manual_grid_square\", manualCellGridDimensions(floatArrayOf(0f, 0f, 80f, 80f), 4).let { \"${it.first}x${it.second}\" })"
        "            .put(\"bitmap_nonwhite_samples\", selfTestBitmapNonwhiteSamples())"
        "            .put(\"labels\", labels))"
        "    }"
@@ -2407,10 +2409,33 @@
        "        focusBoxTypeDropdown(dialog, boxType, showDropdown = true, selectExistingText = true)"
        "    }"
        ""
+       "    private fun manualCellGridDimensions(rect: FloatArray, count: Int): Pair<Int, Int> {"
+       "        val safeCount = count.coerceAtLeast(1)"
+       "        val width = max(1f, rect[2] - rect[0])"
+       "        val height = max(1f, rect[3] - rect[1])"
+       "        val selectionAspect = width.toDouble() / height.toDouble()"
+       "        var bestColumns = 1"
+       "        var bestRows = safeCount"
+       "        var bestScore = Double.POSITIVE_INFINITY"
+       "        for (columns in 1..safeCount) {"
+       "            val rows = ceil(safeCount.toDouble() / columns.toDouble()).toInt().coerceAtLeast(1)"
+       "            val cellAspect = (selectionAspect * rows.toDouble() / columns.toDouble()).coerceAtLeast(0.01)"
+       "            val emptySlots = columns * rows - safeCount"
+       "            val score = kotlin.math.abs(kotlin.math.ln(cellAspect)) + emptySlots.toDouble() / safeCount.toDouble() * 0.35"
+       "            val tie = kotlin.math.abs(score - bestScore) < 0.000001"
+       "            val directionPreferred = if (selectionAspect >= 1.0) columns > bestColumns else columns < bestColumns"
+       "            if (score < bestScore - 0.000001 || (tie && directionPreferred)) {"
+       "                bestColumns = columns"
+       "                bestRows = rows"
+       "                bestScore = score"
+       "            }"
+       "        }"
+       "        return bestColumns to bestRows"
+       "    }"
+       ""
        "    private fun createManualCells(session: SsdSession, rect: FloatArray, count: Int): MutableList<String> {"
        "        val ids = mutableListOf<String>()"
-       "        val columns = ceil(sqrt(count.toDouble())).toInt().coerceAtLeast(1)"
-       "        val rows = ceil(count.toDouble() / columns.toDouble()).toInt().coerceAtLeast(1)"
+       "        val (columns, rows) = manualCellGridDimensions(rect, count)"
        "        val width = max(1f, rect[2] - rect[0])"
        "        val height = max(1f, rect[3] - rect[1])"
        "        for (row in 0 until rows) {"