package com.rizoma.trazo.model import kotlin.math.atan2 import kotlin.math.hypot /** Las 9 direcciones de una tecla: centro (toque) + 8 swipes. */ enum class Dir { C, N, NE, E, SE, S, SW, W, NW } /** * Una tecla del grid 3x3. [center] es el toque; [swipes] mapea cada dirección * de deslizamiento a su carácter. Todo es dato → la distribución se ajusta sin tocar UI. */ data class KeyDef( val center: Char, val swipes: Map = emptyMap(), /** Dirección de swipe que activa mayúsculas en vez de emitir carácter (la R↑). */ val shiftDir: Dir? = null, ) { fun charFor(dir: Dir): Char? = if (dir == Dir.C) center else swipes[dir] } object Layouts { /** * Letras — layout MessagEase ESPAÑOL AUTÉNTICO, mapeado 1:1 desde el teclado * oficial de Exideas (Spanish). Toques A N I / D O R / T E S. * · Acentos en swipe sobre su vocal: á(A↙) í(I↑) é(E←) ó(T↓) ú(T↑). * · ñ en N↑, l en N↓. ü y ç en la tecla D. v en A↘. * · O es el hub: q u p / c · b / g h j (la h, antes tecla propia, va en O↓). * · Puntuación frecuente en la E. La R lleva mayús (shift) con trazo ↑ en la UI. * * Overlay de símbolos en modo letra: cada dirección SIN letra recibe el mismo símbolo * que esa posición de tecla tiene en [symbols] (espejo posicional → el símbolo queda en * el mismo sitio en ambas capas). Los que chocan con una letra viven solo en ?123. * Excepción español: ¿ ¡ chocan con x/l, así que se reubican juntos en la tecla I. */ val letters: List = listOf( // fila superior: A N I (símbolos espejo de 1 2 3) KeyDef('a', mapOf(Dir.S to 'á', Dir.SE to 'v', Dir.SW to '$', Dir.E to '-')), KeyDef( 'n', mapOf( Dir.N to 'ñ', Dir.S to 'l', Dir.NW to '`', Dir.NE to '´', Dir.W to '+', Dir.E to '!', Dir.SW to '/', Dir.SE to '\\', ), ), KeyDef( 'i', mapOf( Dir.NW to 'í', Dir.SW to 'x', Dir.W to '?', Dir.S to '=', Dir.SE to '€', Dir.E to '¿', Dir.NE to '¡', ), ), // fila media: D O R (símbolos espejo de 4 5 6) KeyDef( 'd', mapOf( Dir.N to 'ü', Dir.E to 'k', Dir.S to 'ç', Dir.NW to '{', Dir.NE to '%', Dir.W to '(', Dir.SW to '[', Dir.SE to '_', ), ), KeyDef( 'o', mapOf( Dir.NW to 'q', Dir.N to 'u', Dir.NE to 'p', Dir.E to 'b', Dir.SE to 'j', Dir.S to 'h', Dir.SW to 'g', Dir.W to 'c', ), ), KeyDef( 'r', mapOf( Dir.W to 'm', Dir.NW to '|', Dir.NE to '}', Dir.E to ')', Dir.SW to '@', Dir.SE to ']', ), shiftDir = Dir.N, // R↑ = mayúsculas (OFF→ON→LOCK) ), // fila inferior: T E S (símbolos espejo de 7 8 9) KeyDef( 't', mapOf( Dir.N to 'ú', Dir.NE to 'y', Dir.S to 'ó', Dir.NW to '~', Dir.W to '<', Dir.E to '*', Dir.SE to '\t', ), ), KeyDef( 'e', mapOf( Dir.N to 'w', Dir.W to 'é', Dir.E to 'z', Dir.NE to '\'', Dir.SE to ':', Dir.S to '.', Dir.SW to ',', Dir.NW to '"', ), ), KeyDef( 's', mapOf( Dir.NW to 'f', Dir.N to '&', Dir.NE to '°', Dir.W to '#', Dir.E to '>', Dir.SW to ';', ), ), ) /** * Números y símbolos — capa MessagEase ESPAÑOL AUTÉNTICA (captura oficial de Exideas). * Dígitos 1-9 en marcador telefónico; cada símbolo en su dirección de trazo original. * Desviaciones respecto a la captura (esta app no tiene celda 0 dedicada como MessagEase): * · 0 → 5↓ (la tecla 5 va libre; toca 5 y desliza abajo). * · ¿ ¡ no salen en la captura pero el español los exige: ¡ en 2↓, ¿ en 3↙. */ val symbols: List = listOf( // fila 1: 1 2 3 KeyDef('1', mapOf(Dir.E to '-', Dir.SW to '$')), KeyDef( '2', mapOf( Dir.NW to '`', Dir.N to '^', Dir.NE to '´', Dir.W to '+', Dir.E to '!', Dir.S to '¡', Dir.SW to '/', Dir.SE to '\\', ), ), KeyDef('3', mapOf(Dir.W to '?', Dir.SW to '¿', Dir.S to '=', Dir.SE to '€')), // fila 2: 4 5 6 KeyDef('4', mapOf(Dir.NW to '{', Dir.NE to '%', Dir.W to '(', Dir.SW to '[', Dir.SE to '_')), KeyDef('5', mapOf(Dir.S to '0')), KeyDef('6', mapOf(Dir.NW to '|', Dir.NE to '}', Dir.E to ')', Dir.SW to '@', Dir.SE to ']')), // fila 3: 7 8 9 KeyDef('7', mapOf(Dir.NW to '~', Dir.N to '¨', Dir.W to '<', Dir.E to '*', Dir.SE to '\t')), KeyDef('8', mapOf(Dir.NW to '"', Dir.NE to '\'', Dir.SW to ',', Dir.S to '.', Dir.SE to ':')), KeyDef('9', mapOf(Dir.N to '&', Dir.NE to '°', Dir.W to '#', Dir.E to '>', Dir.SW to ';')), ) } /** Geometría: convierte el delta de un gesto en una de las 9 direcciones. */ fun directionOf(dx: Float, dy: Float, tapRadiusPx: Float): Dir { if (hypot(dx, dy) < tapRadiusPx) return Dir.C var deg = Math.toDegrees(atan2(dy.toDouble(), dx.toDouble())) // 0=E, 90=S (y hacia abajo) if (deg < 0) deg += 360.0 // 8 sectores de 45° centrados en cada eje. val order = arrayOf(Dir.E, Dir.SE, Dir.S, Dir.SW, Dir.W, Dir.NW, Dir.N, Dir.NE) val idx = (((deg + 22.5) / 45.0).toInt()) % 8 return order[idx] }