KeSp_controller/original-src/layout.rs
Mae PUGIN 32ee3a6d26 feat: Complete KeSp Controller — Slint UI port
Full port of the KaSe/KeSp split keyboard configurator from egui to Slint:
- 6 tabs: Keymap, Advanced, Macros, Stats, Settings, Flash
- Responsive keyboard view with scale-to-fit and key rotations
- Key selector popup with categorized grid, MT/LT builders, hex input
- Combo key picker with inline keyboard visual
- Macro step builder with visual tags
- Serial communication via background threads + mpsc polling
- Heatmap overlay with blue-yellow-red gradient
- OTA flasher with prog port VID filtering and partition selector
- WPM polling, Tamagotchi, Autoshift controls
- Dracula theme matching egui version

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 20:40:34 +02:00

286 lines
8.3 KiB
Rust

use serde_json::Value;
/// A keycap with computed absolute position.
#[derive(Clone, Debug, PartialEq)]
pub struct KeycapPos {
pub row: usize,
pub col: usize,
pub x: f32,
pub y: f32,
pub w: f32,
pub h: f32,
pub angle: f32, // degrees
}
const KEY_SIZE: f32 = 50.0;
const KEY_GAP: f32 = 4.0;
/// Parse a layout JSON string into absolute key positions.
pub fn parse_json(json: &str) -> Result<Vec<KeycapPos>, String> {
let val: Value = serde_json::from_str(json)
.map_err(|e| format!("Invalid layout JSON: {}", e))?;
let mut keys = Vec::new();
walk(&val, 0.0, 0.0, 0.0, &mut keys);
if keys.is_empty() {
return Err("No keys found in layout".into());
}
Ok(keys)
}
/// Default layout embedded at compile time.
pub fn default_layout() -> Vec<KeycapPos> {
let json = include_str!("../default.json");
parse_json(json).unwrap_or_default()
}
fn walk(node: &Value, ox: f32, oy: f32, parent_angle: f32, out: &mut Vec<KeycapPos>) {
let obj = match node.as_object() {
Some(o) => o,
None => return,
};
for (key, val) in obj {
let key_str = key.as_str();
match key_str {
"Group" => walk_group(val, ox, oy, parent_angle, out),
"Line" => walk_line(val, ox, oy, parent_angle, out),
"Keycap" => walk_keycap(val, ox, oy, parent_angle, out),
_ => {}
}
}
}
fn parse_margin(val: &Value) -> (f32, f32, f32, f32) {
let as_str = val.as_str();
if let Some(s) = as_str {
let split = s.split(',');
let parts: Vec<f32> = split
.filter_map(|p| {
let trimmed = p.trim();
let parsed = trimmed.parse().ok();
parsed
})
.collect();
let has_four_parts = parts.len() == 4;
if has_four_parts {
return (parts[0], parts[1], parts[2], parts[3]);
}
}
(0.0, 0.0, 0.0, 0.0)
}
fn parse_angle(val: &Value) -> f32 {
let rotate_transform = val.get("RotateTransform");
let angle_val = rotate_transform
.and_then(|rt| rt.get("Angle"));
let angle_f64 = angle_val
.and_then(|a| a.as_f64());
let angle = angle_f64.unwrap_or(0.0) as f32;
angle
}
fn walk_group(val: &Value, ox: f32, oy: f32, parent_angle: f32, out: &mut Vec<KeycapPos>) {
let obj = match val.as_object() {
Some(o) => o,
None => return,
};
let margin_val = obj.get("Margin");
let (ml, mt, _, _) = margin_val
.map(parse_margin)
.unwrap_or_default();
let transform_val = obj.get("RenderTransform");
let angle = transform_val
.map(parse_angle)
.unwrap_or(0.0);
let gx = ox + ml;
let gy = oy + mt;
let children_val = obj.get("Children");
let children_array = children_val
.and_then(|c| c.as_array());
if let Some(children) = children_array {
let combined_angle = parent_angle + angle;
for child in children {
walk(child, gx, gy, combined_angle, out);
}
}
}
fn walk_line(val: &Value, ox: f32, oy: f32, parent_angle: f32, out: &mut Vec<KeycapPos>) {
let obj = match val.as_object() {
Some(o) => o,
None => return,
};
let margin_val = obj.get("Margin");
let (ml, mt, _, _) = margin_val
.map(parse_margin)
.unwrap_or_default();
let transform_val = obj.get("RenderTransform");
let angle = transform_val
.map(parse_angle)
.unwrap_or(0.0);
let total_angle = parent_angle + angle;
let orientation_val = obj.get("Orientation");
let orientation_str = orientation_val
.and_then(|o| o.as_str())
.unwrap_or("Vertical");
let horiz = orientation_str == "Horizontal";
let lx = ox + ml;
let ly = oy + mt;
let rad = total_angle.to_radians();
let cos_a = rad.cos();
let sin_a = rad.sin();
let mut cursor = 0.0f32;
let children_val = obj.get("Children");
let children_array = children_val
.and_then(|c| c.as_array());
if let Some(children) = children_array {
for child in children {
let (cx, cy) = if horiz {
let x = lx + cursor * cos_a;
let y = ly + cursor * sin_a;
(x, y)
} else {
let x = lx - cursor * sin_a;
let y = ly + cursor * cos_a;
(x, y)
};
let child_size = measure(child, horiz);
walk(child, cx, cy, total_angle, out);
cursor += child_size;
}
}
}
/// Measure a child's extent along the parent's main axis.
fn measure(node: &Value, horiz: bool) -> f32 {
let obj = match node.as_object() {
Some(o) => o,
None => return 0.0,
};
for (key, val) in obj {
let key_str = key.as_str();
match key_str {
"Keycap" => {
let width_val = val.get("Width");
let width_f64 = width_val
.and_then(|v| v.as_f64());
let w = width_f64.unwrap_or(KEY_SIZE as f64) as f32;
let extent = if horiz {
w + KEY_GAP
} else {
KEY_SIZE + KEY_GAP
};
return extent;
}
"Line" => {
let sub = match val.as_object() {
Some(o) => o,
None => return 0.0,
};
let sub_orientation = sub.get("Orientation");
let sub_orient_str = sub_orientation
.and_then(|o| o.as_str())
.unwrap_or("Vertical");
let sub_horiz = sub_orient_str == "Horizontal";
let sub_children_val = sub.get("Children");
let sub_children_array = sub_children_val
.and_then(|c| c.as_array());
let children = sub_children_array
.map(|a| a.as_slice())
.unwrap_or(&[]);
let same_direction = sub_horiz == horiz;
let content: f32 = if same_direction {
// Same direction: sum
children
.iter()
.map(|c| measure(c, sub_horiz))
.sum()
} else {
// Cross direction: max
children
.iter()
.map(|c| measure(c, horiz))
.fold(0.0f32, f32::max)
};
return content;
}
"Group" => {
let sub = match val.as_object() {
Some(o) => o,
None => return 0.0,
};
let sub_children_val = sub.get("Children");
let sub_children_array = sub_children_val
.and_then(|c| c.as_array());
let children = sub_children_array
.map(|a| a.as_slice())
.unwrap_or(&[]);
let max_extent = children
.iter()
.map(|c| measure(c, horiz))
.fold(0.0f32, f32::max);
return max_extent;
}
_ => {}
}
}
0.0
}
fn walk_keycap(val: &Value, ox: f32, oy: f32, parent_angle: f32, out: &mut Vec<KeycapPos>) {
let obj = match val.as_object() {
Some(o) => o,
None => return,
};
let col_val = obj.get("Column");
let col_u64 = col_val
.and_then(|v| v.as_u64());
let col = col_u64.unwrap_or(0) as usize;
let row_val = obj.get("Row");
let row_u64 = row_val
.and_then(|v| v.as_u64());
let row = row_u64.unwrap_or(0) as usize;
let width_val = obj.get("Width");
let width_f64 = width_val
.and_then(|v| v.as_f64());
let w = width_f64.unwrap_or(KEY_SIZE as f64) as f32;
let margin_val = obj.get("Margin");
let (ml, mt, _, _) = margin_val
.map(parse_margin)
.unwrap_or_default();
let transform_val = obj.get("RenderTransform");
let angle = transform_val
.map(parse_angle)
.unwrap_or(0.0);
let total_angle = parent_angle + angle;
out.push(KeycapPos {
row,
col,
x: ox + ml,
y: oy + mt,
w,
h: KEY_SIZE,
angle: total_angle,
});
}