use self::SavilleLayout::*;
use core::panic;
use svg::{
node::element::{path::Data, Path},
Document,
};
mod color_palette {
pub const GREY: &str = "#a7a9ab";
pub const WHITE: &str = "#ffffff";
pub const OUT_1: &str = "#3bb6ac";
pub const OUT_2: &str = "#feed02";
pub const OUT_3: &str = "#9085bc";
pub const OUT_4: &str = "#ed7d02";
pub const OUT_5: &str = "#a4daf9";
pub const OUT_6: &str = "#f3b6d1";
pub const OUT_7: &str = "#56358a";
pub const OUT_8: &str = "#e5017c";
pub const OUT_9: &str = "#019fe1";
}
#[derive(Clone, Copy, Default)]
pub enum SavilleLayout {
#[default]
DownRight,
RightDown,
RightDownThin,
}
#[derive(Clone, Copy, Default)]
pub enum SavilleSpaceShape {
#[default]
Fuller,
Regular,
}
enum ParseCharFlag {
None,
SplitVertical { color_left: Option<&'static str> },
}
pub fn encode_saville(
text: &str,
layout: SavilleLayout,
space_shape: SavilleSpaceShape,
symbol_height: i32,
) -> Document {
match layout {
RightDown | RightDownThin => {
let mut paths: Vec<Path> = Vec::new();
let mut parse_char_flag = ParseCharFlag::None;
let mut horizontal_index = 0;
let mut max_horizontal_index = 0;
let mut vertical_index = 0;
for c in text.to_ascii_lowercase().chars() {
if c == '\n' {
if horizontal_index > max_horizontal_index {
max_horizontal_index = horizontal_index;
}
horizontal_index = 0;
vertical_index += 1;
} else {
horizontal_index += char_to_path(
&mut paths,
&mut parse_char_flag,
c,
(
horizontal_index * symbol_height,
vertical_index * symbol_height,
),
symbol_height,
layout,
space_shape,
);
}
}
if horizontal_index > max_horizontal_index {
max_horizontal_index = horizontal_index;
}
vertical_index += 1;
let mut document = Document::new().set(
"viewBox",
(
0,
0,
max_horizontal_index * symbol_height,
match (layout, vertical_index) {
(RightDownThin, 1) => {
(symbol_height as f64 / (2_f64 + 2_f64.sqrt())) as i32
}
_ => vertical_index * symbol_height,
},
),
);
for path in paths {
document = document.add(path);
}
document
}
DownRight => {
let mut paths: Vec<Path> = Vec::new();
let mut parse_char_flag = ParseCharFlag::None;
let mut vertical_index = 0;
let mut max_vertical_index = 0;
let mut horizontal_index = 0;
for c in text.to_ascii_lowercase().chars() {
if c == '\n' {
if vertical_index > max_vertical_index {
max_vertical_index = vertical_index;
}
vertical_index = 0;
horizontal_index += 1;
} else {
vertical_index += char_to_path(
&mut paths,
&mut parse_char_flag,
c,
(
horizontal_index * symbol_height,
vertical_index * symbol_height,
),
symbol_height,
layout,
space_shape,
);
}
}
if vertical_index > max_vertical_index {
max_vertical_index = vertical_index;
}
horizontal_index += 1;
let mut document = Document::new().set(
"viewBox",
(
0,
0,
horizontal_index * symbol_height,
max_vertical_index * symbol_height,
),
);
for path in paths {
document = document.add(path);
}
document
}
}
}
fn octagon_corner_height(symbol_height: i32, space_shape: SavilleSpaceShape) -> i32 {
match space_shape {
SavilleSpaceShape::Regular => (symbol_height as f64 / (2_f64 + 2_f64.sqrt())) as i32,
SavilleSpaceShape::Fuller => (0.22 * symbol_height as f64) as i32,
}
}
pub fn gen_octagon_data(
top_left: (i32, i32),
symbol_height: i32,
space_shape: SavilleSpaceShape,
) -> Data {
let corner_height = octagon_corner_height(symbol_height, space_shape);
let side_len = symbol_height - (2 * corner_height);
Data::new()
.move_to((top_left.0 + corner_height, top_left.1))
.line_by((side_len, 0))
.line_by((corner_height, corner_height))
.line_by((0, side_len))
.line_by((-corner_height, corner_height))
.line_by((-side_len, 0))
.line_by((-corner_height, -corner_height))
.line_by((0, -side_len))
.close()
}
fn gen_square_data(top_left: (i32, i32), symbol_height: i32) -> Data {
Data::new()
.move_to(top_left)
.line_by((0, symbol_height))
.line_by((symbol_height, 0))
.line_by((0, -symbol_height))
.close()
}
fn gen_square_split_vertical_data(top_left: (i32, i32), symbol_height: i32) -> (Data, Data, Data) {
let sides_width = (0.485 * symbol_height as f64) as i32;
let middle_width = symbol_height - (2 * sides_width);
(
Data::new()
.move_to(top_left)
.line_by((0, symbol_height))
.line_by((sides_width, 0))
.line_by((0, -symbol_height))
.close(),
Data::new()
.move_to((top_left.0 + sides_width, top_left.1))
.line_by((0, symbol_height))
.line_by((middle_width, 0))
.line_by((0, -symbol_height))
.close(),
Data::new()
.move_to((top_left.0 + sides_width + middle_width, top_left.1))
.line_by((0, symbol_height))
.line_by((sides_width, 0))
.line_by((0, -symbol_height))
.close(),
)
}
fn gen_octagon_top_data_fuller(
top_left: (i32, i32),
symbol_height: i32,
) -> Data {
let corner_height = octagon_corner_height(symbol_height, SavilleSpaceShape::Fuller);
let small_side_len = octagon_corner_height(symbol_height, SavilleSpaceShape::Regular) - corner_height;
let top_len = symbol_height - (2 * corner_height);
Data::new()
.move_to((top_left.0, top_left.1 + corner_height))
.line_by((corner_height, -corner_height))
.line_by((top_len, 0))
.line_by((corner_height, corner_height))
.line_by((0, small_side_len))
.line_by((-symbol_height, 0))
.close()
}
fn gen_octagon_top_data_regular(
top_left: (i32, i32),
symbol_height: i32,
) -> Data {
let corner_height = octagon_corner_height(symbol_height, SavilleSpaceShape::Regular);
let top_len = symbol_height - (2 * corner_height);
Data::new()
.move_to((top_left.0 + corner_height, top_left.1))
.line_by((top_len, 0))
.line_by((corner_height, corner_height))
.line_by((-symbol_height, 0))
.close()
}
pub fn gen_octagon_top_data(
top_left: (i32, i32),
symbol_height: i32,
space_shape: SavilleSpaceShape,
) -> Data {
match space_shape {
SavilleSpaceShape::Fuller => gen_octagon_top_data_fuller(top_left, symbol_height),
SavilleSpaceShape::Regular => gen_octagon_top_data_regular(top_left, symbol_height),
}
}
fn gen_square_top_data(top_left: (i32, i32), symbol_height: i32) -> Data {
let rectangle_height = (symbol_height as f64 / (2_f64 + 2_f64.sqrt())) as i32;
Data::new()
.move_to(top_left)
.line_by((0, rectangle_height))
.line_by((symbol_height, 0))
.line_by((0, -rectangle_height))
.close()
}
fn gen_square_split_vertical_top_data(
top_left: (i32, i32),
symbol_height: i32,
) -> (Data, Data, Data) {
let rectangle_height = (symbol_height as f64 / (2_f64 + 2_f64.sqrt())) as i32;
let sides_width = (0.485 * symbol_height as f64) as i32;
let middle_width = symbol_height - (2 * sides_width);
(
Data::new()
.move_to(top_left)
.line_by((0, rectangle_height))
.line_by((sides_width, 0))
.line_by((0, -rectangle_height))
.close(),
Data::new()
.move_to((top_left.0 + sides_width, top_left.1))
.line_by((0, rectangle_height))
.line_by((middle_width, 0))
.line_by((0, -rectangle_height))
.close(),
Data::new()
.move_to((top_left.0 + sides_width + middle_width, top_left.1))
.line_by((0, rectangle_height))
.line_by((sides_width, 0))
.line_by((0, -rectangle_height))
.close(),
)
}
fn octagon_data_to_path(paths: &mut Vec<Path>, data: Data, color: &'static str) {
paths.push(Path::new().set("fill", color).set("d", data));
}
fn square_data_to_path(paths: &mut Vec<Path>, data: Data, color: &'static str) {
paths.push(Path::new().set("fill", color).set("d", data));
}
fn square_split_vertical_data_to_path(
paths: &mut Vec<Path>,
data: (Data, Data, Data),
colors: (&'static str, &'static str, &'static str),
) {
paths.push(Path::new().set("fill", colors.0).set("d", data.0));
paths.push(Path::new().set("fill", colors.1).set("d", data.1));
paths.push(Path::new().set("fill", colors.2).set("d", data.2));
}
fn char_to_colors(c: char) -> Option<(&'static str, Option<&'static str>)> {
match c {
' ' => Some((color_palette::GREY, None)),
'0' => Some((color_palette::WHITE, None)),
'1' | 'a' => Some((color_palette::OUT_1, None)),
'2' | 'b' => Some((color_palette::OUT_2, None)),
'3' | 'c' => Some((color_palette::OUT_3, None)),
'4' | 'd' => Some((color_palette::OUT_4, None)),
'5' | 'e' => Some((color_palette::OUT_5, None)),
'6' | 'f' => Some((color_palette::OUT_6, None)),
'7' | 'g' => Some((color_palette::OUT_7, None)),
'8' | 'h' => Some((color_palette::OUT_8, None)),
'9' | 'i' => Some((color_palette::OUT_9, None)),
'j' => Some((color_palette::OUT_1, Some(color_palette::WHITE))),
'k' => Some((color_palette::OUT_1, Some(color_palette::OUT_1))),
'l' => Some((color_palette::OUT_1, Some(color_palette::OUT_2))),
'm' => Some((color_palette::OUT_1, Some(color_palette::OUT_3))),
'n' => Some((color_palette::OUT_1, Some(color_palette::OUT_4))),
'o' => Some((color_palette::OUT_1, Some(color_palette::OUT_5))),
'p' => Some((color_palette::OUT_1, Some(color_palette::OUT_6))),
'q' => Some((color_palette::OUT_1, Some(color_palette::OUT_7))),
'r' => Some((color_palette::OUT_1, Some(color_palette::OUT_8))),
's' => Some((color_palette::OUT_1, Some(color_palette::OUT_9))),
't' => Some((color_palette::OUT_2, Some(color_palette::WHITE))),
'u' => Some((color_palette::OUT_2, Some(color_palette::OUT_1))),
'v' => Some((color_palette::OUT_2, Some(color_palette::OUT_2))),
'w' => Some((color_palette::OUT_2, Some(color_palette::OUT_3))),
'x' => Some((color_palette::OUT_2, Some(color_palette::OUT_4))),
'y' => Some((color_palette::OUT_2, Some(color_palette::OUT_5))),
'z' => Some((color_palette::OUT_2, Some(color_palette::OUT_6))),
_ => None,
}
}
fn char_to_path(
paths: &mut Vec<Path>,
parse_char_flag: &mut ParseCharFlag,
c: char,
top_left: (i32, i32),
symbol_height: i32,
layout: SavilleLayout,
space_shape: SavilleSpaceShape,
) -> i32 {
match parse_char_flag {
ParseCharFlag::None => match c {
'|' => {
*parse_char_flag = ParseCharFlag::SplitVertical { color_left: None };
0
}
c @ ' ' => {
octagon_data_to_path(
paths,
match layout {
DownRight | RightDown => gen_octagon_data(top_left, symbol_height, space_shape),
RightDownThin => gen_octagon_top_data(top_left, symbol_height, space_shape),
},
char_to_colors(c)
.expect("Developer error: `char_to_colors(' ')` should always match to `Some((color, _))`")
.0
);
1
}
c => {
match char_to_colors(c) {
Some((color, None)) => square_data_to_path(
paths,
match layout {
DownRight | RightDown => gen_square_data(top_left, symbol_height),
RightDownThin => gen_square_top_data(top_left, symbol_height),
},
color,
),
Some((color_left, Some(color_right))) => square_split_vertical_data_to_path(
paths,
match layout {
DownRight | RightDown => {
gen_square_split_vertical_data(top_left, symbol_height)
}
RightDownThin => {
gen_square_split_vertical_top_data(top_left, symbol_height)
}
},
(color_left, color_palette::WHITE, color_right),
),
_ => (),
};
1
}
},
ParseCharFlag::SplitVertical { color_left: None } => {
match char_to_colors(c) {
Some((color_left, None)) => {
*parse_char_flag = ParseCharFlag::SplitVertical {
color_left: Some(color_left),
};
}
_ => panic!(
"A character in a vertical split must be in the range `'0'..'9'` or `'a'..'i'`"
),
};
0
}
ParseCharFlag::SplitVertical {
color_left: Some(color_left),
} => {
match char_to_colors(c) {
Some((color_right, None)) => {
square_split_vertical_data_to_path(
paths,
match layout {
DownRight | RightDown => {
gen_square_split_vertical_data(top_left, symbol_height)
}
RightDownThin => {
gen_square_split_vertical_top_data(top_left, symbol_height)
}
},
(color_left, color_palette::WHITE, color_right),
);
*parse_char_flag = ParseCharFlag::None;
}
_ => panic!(
"A character in a vertical split must be in the range `'0'..'9'` or `'a'..'i'`"
),
};
1
}
}
}