1pub const TOUCH_STEPS_PER_KEY: f32 = 20.0;
9pub const TOUCH_OFFSET_LIMIT: i8 = 30;
11const FLAT_NEIGHBOR_REACH: f32 = 3.0;
13const COORDINATE_LIMIT: f32 = 10_000.0;
15
16#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
18pub struct KeyTouch {
19 pub dx: i8,
20 pub dy: i8,
21 pub copy: u8,
22}
23
24#[derive(Clone, Debug, PartialEq)]
26pub struct KeyGeometry {
27 id: u32,
28 unit: (f32, f32),
30 keys: Vec<(u8, f32, f32)>,
32 fingerprint: u64,
34}
35
36pub type KeyRect = [f32; 4];
38
39#[derive(Clone, Copy, Debug, PartialEq, Eq)]
41pub enum KeyGeometryError {
42 ZeroId,
44 InvalidRect,
46 InvalidDuplicate,
48}
49
50impl std::fmt::Display for KeyGeometryError {
51 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52 formatter.write_str(match self {
53 Self::ZeroId => "key geometry id must be non-zero",
54 Self::InvalidRect => {
55 "key geometry needs finite centers, positive sizes and a bounded extent"
56 }
57 Self::InvalidDuplicate => "key geometry duplicates must be one lowercase letter",
58 })
59 }
60}
61
62impl std::error::Error for KeyGeometryError {}
63
64impl KeyGeometry {
65 pub fn new(
67 id: u32,
68 keys: &[KeyRect; 26],
69 semicolon: Option<KeyRect>,
70 duplicates: &[(u8, KeyRect)],
71 ) -> Result<Self, KeyGeometryError> {
72 if id == 0 {
73 return Err(KeyGeometryError::ZeroId);
74 }
75 let valid = |[cx, cy, w, h]: KeyRect| {
76 cx.is_finite() && cy.is_finite() && w.is_finite() && h.is_finite() && w > 0.0 && h > 0.0
77 };
78 if !keys
79 .iter()
80 .chain(semicolon.iter())
81 .chain(duplicates.iter().map(|(_, rect)| rect))
82 .all(|rect| valid(*rect))
83 {
84 return Err(KeyGeometryError::InvalidRect);
85 }
86 if duplicates
87 .iter()
88 .any(|(letter, _)| !letter.is_ascii_lowercase())
89 {
90 return Err(KeyGeometryError::InvalidDuplicate);
91 }
92 let median = |mut values: [f32; 26]| {
93 values.sort_by(f32::total_cmp);
94 values[values.len() / 2]
95 };
96 let unit = (
97 median(keys.map(|[_, _, w, _]| w)),
98 median(keys.map(|[_, _, _, h]| h)),
99 );
100 let normalized = |key: u8, [cx, cy, _, _]: KeyRect| (key, cx / unit.0, cy / unit.1);
101 let mut normalized_keys = Vec::with_capacity(27 + duplicates.len());
102 normalized_keys.extend(
103 (b'a'..=b'z')
104 .zip(keys)
105 .map(|(key, rect)| normalized(key, *rect)),
106 );
107 normalized_keys.extend(semicolon.map(|rect| normalized(b';', rect)));
108 normalized_keys.extend(
109 duplicates
110 .iter()
111 .map(|(letter, rect)| normalized(*letter, *rect)),
112 );
113 if !normalized_keys
115 .iter()
116 .all(|&(_, x, y)| within_limit(x) && within_limit(y))
117 {
118 return Err(KeyGeometryError::InvalidRect);
119 }
120 let fingerprint = content_fingerprint(unit, &normalized_keys);
121 Ok(Self {
122 id,
123 unit,
124 keys: normalized_keys,
125 fingerprint,
126 })
127 }
128
129 pub fn from_json(bytes: &[u8]) -> Result<Option<Self>, String> {
131 #[derive(serde::Deserialize)]
132 #[serde(deny_unknown_fields)]
133 struct Wire {
134 id: u32,
135 #[serde(default)]
136 keys: Vec<KeyRect>,
137 #[serde(default)]
138 semicolon: Option<KeyRect>,
139 #[serde(default)]
140 duplicates: Vec<(String, f32, f32, f32, f32)>,
141 }
142 if bytes.is_empty() {
143 return Ok(None);
144 }
145 let wire: Wire =
146 serde_json::from_slice(bytes).map_err(|_| "invalid key geometry".to_owned())?;
147 if wire.id == 0 {
148 return Ok(None);
149 }
150 let keys: [KeyRect; 26] = wire
151 .keys
152 .try_into()
153 .map_err(|_| "key geometry needs 26 letter keys".to_owned())?;
154 let duplicates = wire
155 .duplicates
156 .into_iter()
157 .map(|(letter, cx, cy, w, h)| match letter.as_bytes() {
158 [key] => Ok((*key, [cx, cy, w, h])),
159 _ => Err(KeyGeometryError::InvalidDuplicate.to_string()),
160 })
161 .collect::<Result<Vec<_>, _>>()?;
162 Self::new(wire.id, &keys, wire.semicolon, &duplicates)
163 .map(Some)
164 .map_err(|error| error.to_string())
165 }
166
167 pub fn id(&self) -> u32 {
168 self.id
169 }
170
171 pub fn fingerprint(&self) -> u64 {
173 self.fingerprint
174 }
175
176 pub fn has_semicolon(&self) -> bool {
178 self.keys.iter().any(|(key, _, _)| *key == b';')
179 }
180
181 fn copies(&self, key: u8) -> impl Iterator<Item = (f32, f32)> + '_ {
183 self.keys
184 .iter()
185 .filter(move |(candidate, _, _)| *candidate == key)
186 .map(|&(_, x, y)| (x, y))
187 }
188
189 fn nearest_copy(&self, key: u8, point: (f32, f32)) -> Option<(f32, f32)> {
191 self.copies(key).min_by(|left, right| {
192 distance_squared(*left, point).total_cmp(&distance_squared(*right, point))
193 })
194 }
195
196 pub fn touch(&self, key: u8, x: f32, y: f32) -> Option<KeyTouch> {
198 let point = (x / self.unit.0, y / self.unit.1);
199 if !within_limit(point.0) || !within_limit(point.1) {
200 return None;
201 }
202 let key = key.to_ascii_lowercase();
203 let (copy, center) = self
204 .copies(key)
205 .enumerate()
206 .min_by(|(_, left), (_, right)| {
207 distance_squared(*left, point).total_cmp(&distance_squared(*right, point))
208 })?;
209 let quantize = |offset: f32| {
210 (offset * TOUCH_STEPS_PER_KEY).round().clamp(
211 f32::from(-TOUCH_OFFSET_LIMIT),
212 f32::from(TOUCH_OFFSET_LIMIT),
213 ) as i8
214 };
215 Some(KeyTouch {
216 dx: quantize(point.0 - center.0),
217 dy: quantize(point.1 - center.1),
218 copy: u8::try_from(copy).ok()?,
219 })
220 }
221
222 fn pressed(&self, key: u8, touch: KeyTouch) -> Option<((f32, f32), (f32, f32))> {
224 let center = self.copies(key).nth(usize::from(touch.copy))?;
225 let point = (
226 center.0 + f32::from(touch.dx) / TOUCH_STEPS_PER_KEY,
227 center.1 + f32::from(touch.dy) / TOUCH_STEPS_PER_KEY,
228 );
229 Some((center, point))
230 }
231
232 pub fn penalty(
234 &self,
235 pressed: u8,
236 touch: KeyTouch,
237 candidate: u8,
238 tuning: &TouchTuning,
239 ) -> Option<f64> {
240 let (center, point) = self.pressed(pressed, touch)?;
241 let other = self.nearest_copy(candidate, point)?;
242 let spread = |(cx, cy): (f32, f32)| {
243 let dx = (f64::from(point.0) - f64::from(cx)) / tuning.sigma_x;
244 let dy = (f64::from(point.1) - f64::from(cy) - tuning.bias_y) / tuning.sigma_y;
245 dx * dx + dy * dy
246 };
247 let raw = tuning.base + tuning.scale * (spread(other) - spread(center));
248 Some(raw.clamp(tuning.min_penalty, tuning.max_penalty))
249 }
250
251 pub fn adjacent(&self, pressed: u8, touch: KeyTouch, candidate: u8) -> bool {
253 let Some((center, _)) = self.pressed(pressed, touch) else {
254 return false;
255 };
256 self.nearest_copy(candidate, center)
257 .is_some_and(|other| distance_squared(center, other).sqrt() <= FLAT_NEIGHBOR_REACH)
258 }
259}
260
261#[derive(Clone, Copy, Debug, PartialEq)]
263pub struct TouchTuning {
264 pub base: f64,
266 pub scale: f64,
268 pub sigma_x: f64,
270 pub sigma_y: f64,
271 pub bias_y: f64,
273 pub min_penalty: f64,
275 pub max_penalty: f64,
276 pub cut: f64,
278 pub show_margin: f64,
280 pub show_penalty: f64,
282 pub lead_margin: f64,
284 pub lead_penalty: f64,
286}
287
288impl Default for TouchTuning {
289 fn default() -> Self {
291 Self {
292 base: 8.0,
293 scale: 12.0,
294 sigma_x: 1.0,
295 sigma_y: 1.0,
296 bias_y: 0.0,
297 min_penalty: 4.0,
298 max_penalty: 28.0,
299 cut: 16.0,
300 show_margin: 6.0,
301 show_penalty: 14.0,
302 lead_margin: f64::INFINITY,
303 lead_penalty: 10.0,
304 }
305 }
306}
307
308fn within_limit(value: f32) -> bool {
310 value.abs() <= COORDINATE_LIMIT
311}
312
313fn content_fingerprint(unit: (f32, f32), keys: &[(u8, f32, f32)]) -> u64 {
315 use std::hash::{Hash, Hasher};
316 let bits = |value: f32| (value + 0.0).to_bits();
317 let mut hasher = std::collections::hash_map::DefaultHasher::new();
318 bits(unit.0).hash(&mut hasher);
319 bits(unit.1).hash(&mut hasher);
320 for &(key, x, y) in keys {
321 key.hash(&mut hasher);
322 bits(x).hash(&mut hasher);
323 bits(y).hash(&mut hasher);
324 }
325 hasher.finish()
326}
327
328fn distance_squared((ax, ay): (f32, f32), (bx, by): (f32, f32)) -> f32 {
329 (ax - bx) * (ax - bx) + (ay - by) * (ay - by)
330}
331
332#[cfg(test)]
333mod tests;