pub const TOUCH_STEPS_PER_KEY: f32 = 20.0;
pub const TOUCH_OFFSET_LIMIT: i8 = 30;
const FLAT_NEIGHBOR_REACH: f32 = 3.0;
const COORDINATE_LIMIT: f32 = 10_000.0;
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct KeyTouch {
pub dx: i8,
pub dy: i8,
pub copy: u8,
}
#[derive(Clone, Debug, PartialEq)]
pub struct KeyGeometry {
id: u32,
unit: (f32, f32),
keys: Vec<(u8, f32, f32)>,
fingerprint: u64,
}
pub type KeyRect = [f32; 4];
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum KeyGeometryError {
ZeroId,
InvalidRect,
InvalidDuplicate,
}
impl std::fmt::Display for KeyGeometryError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(match self {
Self::ZeroId => "key geometry id must be non-zero",
Self::InvalidRect => {
"key geometry needs finite centers, positive sizes and a bounded extent"
}
Self::InvalidDuplicate => "key geometry duplicates must be one lowercase letter",
})
}
}
impl std::error::Error for KeyGeometryError {}
impl KeyGeometry {
pub fn new(
id: u32,
keys: &[KeyRect; 26],
semicolon: Option<KeyRect>,
duplicates: &[(u8, KeyRect)],
) -> Result<Self, KeyGeometryError> {
if id == 0 {
return Err(KeyGeometryError::ZeroId);
}
let valid = |[cx, cy, w, h]: KeyRect| {
cx.is_finite() && cy.is_finite() && w.is_finite() && h.is_finite() && w > 0.0 && h > 0.0
};
if !keys
.iter()
.chain(semicolon.iter())
.chain(duplicates.iter().map(|(_, rect)| rect))
.all(|rect| valid(*rect))
{
return Err(KeyGeometryError::InvalidRect);
}
if duplicates
.iter()
.any(|(letter, _)| !letter.is_ascii_lowercase())
{
return Err(KeyGeometryError::InvalidDuplicate);
}
let median = |mut values: [f32; 26]| {
values.sort_by(f32::total_cmp);
values[values.len() / 2]
};
let unit = (
median(keys.map(|[_, _, w, _]| w)),
median(keys.map(|[_, _, _, h]| h)),
);
let normalized = |key: u8, [cx, cy, _, _]: KeyRect| (key, cx / unit.0, cy / unit.1);
let mut normalized_keys = Vec::with_capacity(27 + duplicates.len());
normalized_keys.extend(
(b'a'..=b'z')
.zip(keys)
.map(|(key, rect)| normalized(key, *rect)),
);
normalized_keys.extend(semicolon.map(|rect| normalized(b';', rect)));
normalized_keys.extend(
duplicates
.iter()
.map(|(letter, rect)| normalized(*letter, *rect)),
);
if !normalized_keys
.iter()
.all(|&(_, x, y)| within_limit(x) && within_limit(y))
{
return Err(KeyGeometryError::InvalidRect);
}
let fingerprint = content_fingerprint(unit, &normalized_keys);
Ok(Self {
id,
unit,
keys: normalized_keys,
fingerprint,
})
}
pub fn from_json(bytes: &[u8]) -> Result<Option<Self>, String> {
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct Wire {
id: u32,
#[serde(default)]
keys: Vec<KeyRect>,
#[serde(default)]
semicolon: Option<KeyRect>,
#[serde(default)]
duplicates: Vec<(String, f32, f32, f32, f32)>,
}
if bytes.is_empty() {
return Ok(None);
}
let wire: Wire =
serde_json::from_slice(bytes).map_err(|_| "invalid key geometry".to_owned())?;
if wire.id == 0 {
return Ok(None);
}
let keys: [KeyRect; 26] = wire
.keys
.try_into()
.map_err(|_| "key geometry needs 26 letter keys".to_owned())?;
let duplicates = wire
.duplicates
.into_iter()
.map(|(letter, cx, cy, w, h)| match letter.as_bytes() {
[key] => Ok((*key, [cx, cy, w, h])),
_ => Err(KeyGeometryError::InvalidDuplicate.to_string()),
})
.collect::<Result<Vec<_>, _>>()?;
Self::new(wire.id, &keys, wire.semicolon, &duplicates)
.map(Some)
.map_err(|error| error.to_string())
}
pub fn id(&self) -> u32 {
self.id
}
pub fn fingerprint(&self) -> u64 {
self.fingerprint
}
pub fn has_semicolon(&self) -> bool {
self.keys.iter().any(|(key, _, _)| *key == b';')
}
fn copies(&self, key: u8) -> impl Iterator<Item = (f32, f32)> + '_ {
self.keys
.iter()
.filter(move |(candidate, _, _)| *candidate == key)
.map(|&(_, x, y)| (x, y))
}
fn nearest_copy(&self, key: u8, point: (f32, f32)) -> Option<(f32, f32)> {
self.copies(key).min_by(|left, right| {
distance_squared(*left, point).total_cmp(&distance_squared(*right, point))
})
}
pub fn touch(&self, key: u8, x: f32, y: f32) -> Option<KeyTouch> {
let point = (x / self.unit.0, y / self.unit.1);
if !within_limit(point.0) || !within_limit(point.1) {
return None;
}
let key = key.to_ascii_lowercase();
let (copy, center) = self
.copies(key)
.enumerate()
.min_by(|(_, left), (_, right)| {
distance_squared(*left, point).total_cmp(&distance_squared(*right, point))
})?;
let quantize = |offset: f32| {
(offset * TOUCH_STEPS_PER_KEY).round().clamp(
f32::from(-TOUCH_OFFSET_LIMIT),
f32::from(TOUCH_OFFSET_LIMIT),
) as i8
};
Some(KeyTouch {
dx: quantize(point.0 - center.0),
dy: quantize(point.1 - center.1),
copy: u8::try_from(copy).ok()?,
})
}
fn pressed(&self, key: u8, touch: KeyTouch) -> Option<((f32, f32), (f32, f32))> {
let center = self.copies(key).nth(usize::from(touch.copy))?;
let point = (
center.0 + f32::from(touch.dx) / TOUCH_STEPS_PER_KEY,
center.1 + f32::from(touch.dy) / TOUCH_STEPS_PER_KEY,
);
Some((center, point))
}
pub fn penalty(
&self,
pressed: u8,
touch: KeyTouch,
candidate: u8,
tuning: &TouchTuning,
) -> Option<f64> {
let (center, point) = self.pressed(pressed, touch)?;
let other = self.nearest_copy(candidate, point)?;
let spread = |(cx, cy): (f32, f32)| {
let dx = (f64::from(point.0) - f64::from(cx)) / tuning.sigma_x;
let dy = (f64::from(point.1) - f64::from(cy) - tuning.bias_y) / tuning.sigma_y;
dx * dx + dy * dy
};
let raw = tuning.base + tuning.scale * (spread(other) - spread(center));
Some(raw.clamp(tuning.min_penalty, tuning.max_penalty))
}
pub fn adjacent(&self, pressed: u8, touch: KeyTouch, candidate: u8) -> bool {
let Some((center, _)) = self.pressed(pressed, touch) else {
return false;
};
self.nearest_copy(candidate, center)
.is_some_and(|other| distance_squared(center, other).sqrt() <= FLAT_NEIGHBOR_REACH)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct TouchTuning {
pub base: f64,
pub scale: f64,
pub sigma_x: f64,
pub sigma_y: f64,
pub bias_y: f64,
pub min_penalty: f64,
pub max_penalty: f64,
pub cut: f64,
pub show_margin: f64,
pub show_penalty: f64,
pub lead_margin: f64,
pub lead_penalty: f64,
}
impl Default for TouchTuning {
fn default() -> Self {
Self {
base: 8.0,
scale: 12.0,
sigma_x: 1.0,
sigma_y: 1.0,
bias_y: 0.0,
min_penalty: 4.0,
max_penalty: 28.0,
cut: 16.0,
show_margin: 6.0,
show_penalty: 14.0,
lead_margin: f64::INFINITY,
lead_penalty: 10.0,
}
}
}
fn within_limit(value: f32) -> bool {
value.abs() <= COORDINATE_LIMIT
}
fn content_fingerprint(unit: (f32, f32), keys: &[(u8, f32, f32)]) -> u64 {
use std::hash::{Hash, Hasher};
let bits = |value: f32| (value + 0.0).to_bits();
let mut hasher = std::collections::hash_map::DefaultHasher::new();
bits(unit.0).hash(&mut hasher);
bits(unit.1).hash(&mut hasher);
for &(key, x, y) in keys {
key.hash(&mut hasher);
bits(x).hash(&mut hasher);
bits(y).hash(&mut hasher);
}
hasher.finish()
}
fn distance_squared((ax, ay): (f32, f32), (bx, by): (f32, f32)) -> f32 {
(ax - bx) * (ax - bx) + (ay - by) * (ay - by)
}
#[cfg(test)]
mod tests;