use std::collections::{HashMap, HashSet};
use std::ops::Range;
use minidom::Element;
use crate::board_label_text;
use crate::board_side::{BoardSide, BoardSideSet};
use crate::errors::MakeSvgError;
use crate::goban::{Goban, Stone, StoneColor};
use crate::goban_range::GobanRange;
use crate::goban_style::GobanStyle;
use crate::node_description::NodeDescription;
pub static NAMESPACE: &str = "http://www.w3.org/2000/svg";
static BOARD_MARGIN: f64 = 0.64;
static LABEL_MARGIN: f64 = 0.8;
static REPEATED_MOVES_MARGIN: f64 = 0.32;
static FONT_FAMILY: &str = "Inter";
static FONT_SIZE: f64 = 0.45;
static FONT_WEIGHT: usize = 700;
#[derive(Debug, Clone, Default)]
pub struct MakeSvgOptions {
pub node_description: NodeDescription,
pub goban_range: GobanRange,
pub style: GobanStyle,
pub viewbox_width: f64,
pub label_sides: BoardSideSet,
pub move_number_options: Option<MoveNumberOptions>,
pub draw_marks: bool,
pub draw_triangles: bool,
pub draw_circles: bool,
pub draw_squares: bool,
pub draw_selected: bool,
pub draw_dimmed: bool,
pub draw_labels: bool,
pub draw_lines: bool,
pub draw_arrows: bool,
pub kifu_mode: bool,
}
#[derive(Debug, Clone, Copy)]
pub struct MoveNumberOptions {
pub start: u64,
pub end: Option<u64>,
pub count_from: u64,
}
pub fn make_svg(goban: &Goban, options: &MakeSvgOptions) -> Result<Element, MakeSvgError> {
let (x_range, y_range) = options.goban_range.get_ranges(goban, options)?;
let width = x_range.end - x_range.start;
let height = y_range.end - y_range.start;
if !options.label_sides.is_empty() && width > 25 || height > 99 {
return Err(MakeSvgError::UnlabellableRange);
}
let (top_margin, right_margin, bottom_margin, left_margin) = get_margins(&options.label_sides);
let definitions = {
let clip_path = Element::builder("clipPath", NAMESPACE)
.attr("id", "board-clip")
.append(
Element::builder("rect", NAMESPACE)
.attr("x", format_float(f64::from(x_range.start) - 0.5))
.attr("y", format_float(f64::from(y_range.start) - 0.5))
.attr("width", width.to_string())
.attr("height", height.to_string())
.build(),
)
.build();
Element::builder("defs", NAMESPACE)
.append(clip_path)
.append_all(options.style.defs()?)
.build()
};
let diagram_width = f64::from(width) - 1.0 + 2.0 * BOARD_MARGIN + left_margin + right_margin;
let (diagram, diagram_height) = {
let board = build_board(goban, options);
let board_view = {
let board_view_transform = format!(
"translate({}, {})",
format_float(BOARD_MARGIN + left_margin - f64::from(x_range.start)),
format_float(BOARD_MARGIN + top_margin - f64::from(y_range.start))
);
Element::builder("g", NAMESPACE)
.attr("id", "board-view")
.attr("transform", board_view_transform)
.append(board)
.build()
};
let scale = format_float(options.viewbox_width / diagram_width);
let transform = format!("scale({}, {})", scale, scale);
let mut diagram_builder = Element::builder("g", NAMESPACE)
.attr("id", "diagram")
.attr("transform", transform)
.append(board_view);
if !options.label_sides.is_empty() {
let goban_size = goban.size();
diagram_builder = diagram_builder.append(draw_board_labels(
x_range,
goban_size.1 - height - y_range.start + 1..goban_size.1 - y_range.start + 1,
options,
));
}
let mut diagram_height =
f64::from(height) - 1.0 + 2.0 * BOARD_MARGIN + top_margin + bottom_margin;
if options.kifu_mode {
if let Some((element, element_height)) = draw_repeated_stones(
goban,
width,
diagram_height + REPEATED_MOVES_MARGIN,
options,
) {
diagram_builder = diagram_builder.append(element);
diagram_height += element_height + REPEATED_MOVES_MARGIN * 2.0;
}
}
(diagram_builder.build(), diagram_height)
};
let background = Element::builder("rect", NAMESPACE)
.attr("fill", options.style.background_fill())
.attr("height", "100%")
.attr("width", "100%")
.attr("x", "0")
.attr("y", "0")
.build();
let viewbox_height = options.viewbox_width * diagram_height / diagram_width;
let viewbox_attr = format!(
"0 0 {} {}",
format_float(options.viewbox_width),
format_float(viewbox_height)
);
let svg = Element::builder("svg", NAMESPACE)
.attr("viewBox", viewbox_attr)
.attr("width", options.viewbox_width.to_string())
.attr("font-size", FONT_SIZE.to_string())
.attr("font-family", FONT_FAMILY)
.attr("font-weight", FONT_WEIGHT)
.append(definitions)
.append(background)
.append(diagram)
.build();
Ok(svg)
}
fn build_board(goban: &Goban, options: &MakeSvgOptions) -> Element {
let mut group_builder = Element::builder("g", NAMESPACE)
.attr("id", "goban")
.attr("clip-path", "url(#board-clip)")
.append(build_board_lines_group(goban, options))
.append(build_stones_group(goban, options));
let move_numbers = get_move_numbers(goban, options);
let no_markup_points: HashSet<(u8, u8)> = move_numbers
.iter()
.map(|(_, stone)| (stone.x, stone.y))
.collect();
if let Some(move_number_options) = &options.move_number_options {
group_builder = group_builder.append(build_move_numbers_group(
goban,
options,
move_number_options,
&move_numbers,
));
}
if options.draw_marks {
group_builder = group_builder.append(build_marks_group(goban, options, &no_markup_points));
}
if options.draw_triangles {
group_builder =
group_builder.append(build_triangles_group(goban, options, &no_markup_points));
}
if options.draw_circles {
group_builder =
group_builder.append(build_circles_group(goban, options, &no_markup_points));
}
if options.draw_squares {
group_builder =
group_builder.append(build_squares_group(goban, options, &no_markup_points));
}
if options.draw_selected {
group_builder =
group_builder.append(build_selected_group(goban, options, &no_markup_points));
}
if options.draw_dimmed {
group_builder = group_builder.append(build_dimmed_group(goban, options));
}
if options.draw_labels {
group_builder = group_builder.append(build_label_group(goban, options, &no_markup_points));
}
if options.draw_lines {
group_builder = group_builder.append(build_line_group(goban, options));
}
if options.draw_arrows {
group_builder = group_builder.append(build_arrow_group(goban, options));
}
group_builder.build()
}
fn build_board_lines_group(goban: &Goban, options: &MakeSvgOptions) -> Element {
let mut group_builder = Element::builder("g", NAMESPACE)
.attr("id", "lines")
.attr("stroke", options.style.line_color())
.attr("stroke-width", format_float(options.style.line_width()))
.attr("stroke-linecap", "square");
let goban_size = goban.size();
for x in 0..goban_size.0 as usize {
group_builder = group_builder.append(
Element::builder("line", NAMESPACE)
.attr("x1", x.to_string())
.attr("y1", "0")
.attr("x2", x.to_string())
.attr("y2", (goban_size.1 - 1).to_string()),
);
}
for y in 0..goban_size.1 as usize {
group_builder = group_builder.append(
Element::builder("line", NAMESPACE)
.attr("x1", 0.to_string())
.attr("y1", y.to_string())
.attr("x2", (goban_size.0 - 1).to_string())
.attr("y2", y.to_string()),
);
}
let hoshi_radius = options.style.hoshi_radius();
let mut hoshi = Element::builder("g", NAMESPACE)
.attr("id", "hoshi")
.attr("stroke", "none")
.attr("fill", options.style.line_color());
for (x, y) in goban.hoshi_points() {
hoshi = hoshi.append(
Element::builder("circle", NAMESPACE)
.attr("cx", x.to_string())
.attr("cy", y.to_string())
.attr("r", format_float(hoshi_radius)),
);
}
group_builder.append(hoshi).build()
}
fn build_stones_group(goban: &Goban, options: &MakeSvgOptions) -> Element {
let mut group_builder = Element::builder("g", NAMESPACE)
.attr("id", "stones")
.attr("stroke", "none");
let mut stones: Vec<Stone> = if options.kifu_mode {
let mut stones: HashMap<(u8, u8), Stone> = HashMap::new();
let mut numbered_stones: HashMap<(u8, u8), Stone> = HashMap::new();
for (n, stone) in goban.moves() {
if n >= options.move_number_options.unwrap().start {
numbered_stones.entry((stone.x, stone.y)).or_insert(stone);
}
stones.insert((stone.x, stone.y), stone);
}
for (key, stone) in numbered_stones {
stones.insert(key, stone);
}
stones.into_values().collect()
} else {
goban.stones().collect()
};
stones.sort_by_key(|stone| (stone.y, stone.x));
for stone in stones {
group_builder = group_builder.append(draw_stone(stone, &options.style));
}
group_builder.build()
}
fn build_move_numbers_group(
goban: &Goban,
options: &MakeSvgOptions,
move_number_options: &MoveNumberOptions,
move_numbers: &[(u64, Stone)],
) -> Element {
let mut group_builder = Element::builder("g", NAMESPACE)
.attr("id", "move-numbers")
.attr("text-anchor", "middle");
for (n, stone) in move_numbers {
let stone_color = if options.kifu_mode {
Some(stone.color)
} else {
goban.stone_color(stone.x, stone.y)
};
let move_number = n + move_number_options.count_from - move_number_options.start;
group_builder = group_builder.append(draw_move_number(
stone.x,
stone.y,
move_number,
stone_color,
&options.style,
));
}
group_builder.build()
}
fn get_move_numbers(goban: &Goban, options: &MakeSvgOptions) -> Vec<(u64, Stone)> {
let move_number_options = match options.move_number_options {
Some(move_number_options) => move_number_options,
None => return Vec::new(),
};
let numbered_moves = goban
.moves()
.skip_while(|&(n, _)| n < move_number_options.start)
.take_while(|&(n, _)| move_number_options.end.map(|end| n <= end).unwrap_or(true));
let mut move_numbers: HashMap<(u8, u8), (u64, Stone)> = HashMap::new();
for (n, stone) in numbered_moves {
if options.kifu_mode {
move_numbers.entry((stone.x, stone.y)).or_insert((n, stone));
} else {
move_numbers.insert((stone.x, stone.y), (n, stone));
}
}
let mut move_numbers: Vec<(u64, Stone)> = move_numbers.values().copied().collect();
move_numbers.sort_unstable_by_key(|(n, _)| *n);
move_numbers
}
fn build_marks_group(
goban: &Goban,
options: &MakeSvgOptions,
no_markup_points: &HashSet<(u8, u8)>,
) -> Element {
let mut group_builder = Element::builder("g", NAMESPACE).attr("id", "markup-marks");
let mut marks: Vec<_> = goban.marks().collect();
marks.sort_unstable();
for point in marks.iter().filter(|p| !no_markup_points.contains(p)) {
let stone_color = goban.stone_color(point.0, point.1);
group_builder =
group_builder.append(draw_mark(point.0, point.1, stone_color, &options.style));
}
group_builder.build()
}
fn build_triangles_group(
goban: &Goban,
options: &MakeSvgOptions,
no_markup_points: &HashSet<(u8, u8)>,
) -> Element {
let mut group_builder = Element::builder("g", NAMESPACE).attr("id", "markup-triangles");
let mut triangles: Vec<_> = goban.triangles().collect();
triangles.sort_unstable();
for point in triangles.iter().filter(|p| !no_markup_points.contains(p)) {
let stone_color = goban.stone_color(point.0, point.1);
group_builder =
group_builder.append(draw_triangle(point.0, point.1, stone_color, &options.style));
}
group_builder.build()
}
fn build_circles_group(
goban: &Goban,
options: &MakeSvgOptions,
no_markup_points: &HashSet<(u8, u8)>,
) -> Element {
let mut group_builder = Element::builder("g", NAMESPACE).attr("id", "markup-circles");
let mut circles: Vec<_> = goban.circles().collect();
circles.sort_unstable();
for point in circles.iter().filter(|p| !no_markup_points.contains(p)) {
let stone_color = goban.stone_color(point.0, point.1);
group_builder =
group_builder.append(draw_circle(point.0, point.1, stone_color, &options.style));
}
group_builder.build()
}
fn build_squares_group(
goban: &Goban,
options: &MakeSvgOptions,
no_markup_points: &HashSet<(u8, u8)>,
) -> Element {
let mut group_builder = Element::builder("g", NAMESPACE).attr("id", "markup-squares");
let mut squares: Vec<_> = goban.squares().collect();
squares.sort_unstable();
for point in squares.iter().filter(|p| !no_markup_points.contains(p)) {
let stone_color = goban.stone_color(point.0, point.1);
group_builder =
group_builder.append(draw_square(point.0, point.1, stone_color, &options.style));
}
group_builder.build()
}
fn build_selected_group(
goban: &Goban,
options: &MakeSvgOptions,
no_markup_points: &HashSet<(u8, u8)>,
) -> Element {
let mut group_builder = Element::builder("g", NAMESPACE).attr("id", "markup-selected");
let mut selected: Vec<_> = goban.selected().collect();
selected.sort_unstable();
for point in selected.iter().filter(|p| !no_markup_points.contains(p)) {
let stone_color = goban.stone_color(point.0, point.1);
group_builder =
group_builder.append(draw_selected(point.0, point.1, stone_color, &options.style));
}
group_builder.build()
}
fn build_dimmed_group(goban: &Goban, _options: &MakeSvgOptions) -> Element {
let mut group_builder = Element::builder("g", NAMESPACE).attr("id", "markup-dimmed");
let mut dimmed: Vec<_> = goban.dimmed().collect();
dimmed.sort_unstable();
for point in dimmed {
group_builder = group_builder.append(dim_square(point.0, point.1));
}
group_builder.build()
}
fn build_label_group(
goban: &Goban,
options: &MakeSvgOptions,
no_markup_points: &HashSet<(u8, u8)>,
) -> Element {
let mut group_builder = Element::builder("g", NAMESPACE).attr("id", "markup-labels");
let mut labels: Vec<_> = goban.labels().collect();
labels.sort_unstable();
for (point, text) in labels.iter().filter(|(p, _)| !no_markup_points.contains(p)) {
let stone_color = goban.stone_color(point.0, point.1);
group_builder = group_builder.append(draw_label(
point.0,
point.1,
text,
stone_color,
&options.style,
));
}
group_builder.build()
}
fn build_line_group(goban: &Goban, options: &MakeSvgOptions) -> Element {
let mut group_builder = Element::builder("g", NAMESPACE)
.attr("id", "markup-lines")
.attr("stroke", "black")
.attr("stroke-width", format_float(options.style.line_width()))
.attr("marker-start", "url(#linehead)")
.attr("marker-end", "url(#linehead)");
let mut lines: Vec<_> = goban.lines().collect();
lines.sort_unstable();
for (p1, p2) in lines {
group_builder = group_builder.append(
Element::builder("line", NAMESPACE)
.attr("x1", p1.0)
.attr("x2", p2.0)
.attr("y1", p1.1)
.attr("y2", p2.1),
);
}
group_builder.build()
}
fn build_arrow_group(goban: &Goban, options: &MakeSvgOptions) -> Element {
let mut group_builder = Element::builder("g", NAMESPACE)
.attr("id", "markup-arrows")
.attr("stroke", "black")
.attr("stroke-width", format_float(options.style.line_width()))
.attr("marker-end", "url(#arrowhead)");
let mut arrows: Vec<_> = goban.arrows().collect();
arrows.sort_unstable();
for (p1, p2) in arrows {
group_builder = group_builder.append(
Element::builder("line", NAMESPACE)
.attr("x1", p1.0)
.attr("x2", p2.0)
.attr("y1", p1.1)
.attr("y2", p2.1),
);
}
group_builder.build()
}
fn draw_board_labels(x_range: Range<u8>, y_range: Range<u8>, options: &MakeSvgOptions) -> Element {
let (top_margin, _, _, left_margin) = get_margins(&options.label_sides);
let transform = format!(
"translate({}, {})",
format_float(left_margin),
format_float(top_margin)
);
let mut group_builder = Element::builder("g", NAMESPACE)
.attr("id", "board-labels")
.attr("fill", options.style.label_color())
.attr("transform", transform);
if options.label_sides.contains(BoardSide::North) {
let mut builder = Element::builder("g", NAMESPACE).attr("text-anchor", "middle");
let start = x_range.start;
for x in x_range.clone() {
builder = builder.append(
Element::builder("text", NAMESPACE)
.attr("x", format_float(f64::from(x - start) + BOARD_MARGIN))
.attr("y", "0")
.append(board_label_text(x))
.build(),
);
}
group_builder = group_builder.append(builder);
};
if options.label_sides.contains(BoardSide::West) {
let mut builder = Element::builder("g", NAMESPACE).attr("text-anchor", "end");
let end = y_range.end;
for y in y_range.clone() {
builder = builder.append(
Element::builder("text", NAMESPACE)
.attr("x", "0")
.attr("y", format_float(f64::from(end - y - 1) + BOARD_MARGIN))
.attr("dy", "0.35em")
.append(y.to_string())
.build(),
);
}
group_builder = group_builder.append(builder);
};
if options.label_sides.contains(BoardSide::South) {
let mut builder = Element::builder("g", NAMESPACE).attr("text-anchor", "middle");
let start = x_range.start;
let y = f64::from(y_range.end - y_range.start + 1) - BOARD_MARGIN;
for x in x_range.clone() {
builder = builder.append(
Element::builder("text", NAMESPACE)
.attr("x", format_float(f64::from(x - start) + BOARD_MARGIN))
.attr("y", format_float(y))
.attr("alignment-baseline", "hanging")
.append(board_label_text(x))
.build(),
);
}
group_builder = group_builder.append(builder);
};
if options.label_sides.contains(BoardSide::East) {
let mut builder = Element::builder("g", NAMESPACE).attr("text-anchor", "start");
let end = y_range.end;
let x = f64::from(x_range.end - x_range.start + 1) - BOARD_MARGIN;
for y in y_range {
builder = builder.append(
Element::builder("text", NAMESPACE)
.attr("x", format_float(x))
.attr("y", format_float(f64::from(end - y - 1) + BOARD_MARGIN))
.attr("dy", "0.35em")
.append(y.to_string())
.build(),
);
}
group_builder = group_builder.append(builder);
};
group_builder.build()
}
fn draw_repeated_stones(
goban: &Goban,
width: u8,
diagram_height: f64,
options: &MakeSvgOptions,
) -> Option<(Element, f64)> {
let entry_padding = 0.25;
let entry_width = 2.43;
let entry_height = 0.4;
let width = f64::from(width);
let (_, _, _, left_margin) = get_margins(&options.label_sides);
let columns = ((width - 1.0 - (2.0 * entry_padding)) / entry_width).floor() as usize;
let x = BOARD_MARGIN
+ left_margin
+ entry_padding
+ (width - 1.0 - 2.0 * entry_padding - entry_width * f64::from(columns as u32)) / 2.0;
let y = diagram_height + entry_padding + entry_height;
let mut text_builder = Element::builder("text", NAMESPACE)
.attr("y", format_float(y))
.attr("font-size", format_float(entry_height))
.attr("fill", options.style.line_color()); let move_number_options = options.move_number_options.unwrap();
let repeated_moves: Vec<(u64, u64)> = {
let mut repeated_moves = Vec::new();
let mut seen_moves: HashMap<(u8, u8), u64> = HashMap::new();
for (n, stone) in goban.moves() {
match seen_moves.entry((stone.x, stone.y)) {
std::collections::hash_map::Entry::Occupied(mut entry) => {
if *entry.get() < move_number_options.start {
entry.insert(n);
}
let value = *entry.get();
let in_range = value >= move_number_options.start
&& move_number_options
.end
.map(|end| value <= end)
.unwrap_or(true);
if n > value && in_range {
repeated_moves.push((
n + move_number_options.count_from - move_number_options.start,
value + move_number_options.count_from - move_number_options.start,
));
}
}
std::collections::hash_map::Entry::Vacant(entry) => {
entry.insert(n);
}
}
}
repeated_moves.sort_unstable();
repeated_moves
};
if repeated_moves.is_empty() {
return None;
}
let rows = (f64::from(repeated_moves.len() as u32) / f64::from(columns as u32)).ceil();
for (i, (move_num, original)) in repeated_moves.iter().enumerate() {
let column = f64::from((i % columns) as u32);
let mut tspan_builder = Element::builder("tspan", NAMESPACE)
.append(format!("{}→{}", move_num, original))
.attr("x", format_float(x + entry_width * column));
if i % columns == 0 && i != 0 {
tspan_builder = tspan_builder.attr("dy", format_float(entry_height));
}
text_builder = text_builder.append(tspan_builder);
}
let rect_height = 2.0 * entry_padding + entry_height * rows + options.style.line_width() * 2.0;
let group = Element::builder("g", NAMESPACE)
.attr("id", "repeated-stones")
.append(
Element::builder("rect", NAMESPACE)
.attr("fill", "white")
.attr("stroke", options.style.line_color())
.attr("stroke-width", format_float(options.style.line_width()))
.attr("x", format_float(BOARD_MARGIN + left_margin))
.attr("y", format_float(diagram_height))
.attr("width", format_float(width - 1.0))
.attr("height", format_float(rect_height)),
)
.append(text_builder)
.build();
Some((group, rect_height))
}
fn draw_stone(stone: Stone, style: &GobanStyle) -> Element {
let mut circle_builder = Element::builder("circle", NAMESPACE)
.attr("cx", stone.x)
.attr("cy", stone.y)
.attr("r", "0.48");
if let Some(stroke) = style.stone_stroke(stone.color) {
circle_builder = circle_builder
.attr("stroke", stroke)
.attr("stroke-width", format_float(style.line_width()))
}
if let Some(fill) = style.stone_fill(stone.color) {
circle_builder = circle_builder.attr("fill", fill);
}
circle_builder.build()
}
fn draw_move_number(
x: u8,
y: u8,
n: u64,
color: Option<StoneColor>,
style: &GobanStyle,
) -> Element {
let text_element = Element::builder("text", NAMESPACE)
.attr("x", x)
.attr("y", y)
.attr("dy", "0.35em")
.attr("fill", style.markup_color(color))
.append(n.to_string());
let mut group_builder = Element::builder("g", NAMESPACE);
if color.is_none() {
group_builder = group_builder.append(
Element::builder("rect", NAMESPACE)
.attr("fill", style.background_fill())
.attr("x", format_float(f64::from(x) - 0.4))
.attr("y", format_float(f64::from(y) - 0.4))
.attr("width", "0.8")
.attr("height", "0.8"),
);
}
group_builder.append(text_element).build()
}
fn draw_mark(x: u8, y: u8, color: Option<StoneColor>, style: &GobanStyle) -> Element {
Element::builder("g", NAMESPACE)
.attr("stroke", style.markup_color(color))
.attr("stroke-width", style.markup_stroke_width().to_string())
.append(
Element::builder("line", NAMESPACE)
.attr("x1", format_float(f64::from(x) - 0.25))
.attr("x2", format_float(f64::from(x) + 0.25))
.attr("y1", format_float(f64::from(y) - 0.25))
.attr("y2", format_float(f64::from(y) + 0.25)),
)
.append(
Element::builder("line", NAMESPACE)
.attr("x1", format_float(f64::from(x) - 0.25))
.attr("x2", format_float(f64::from(x) + 0.25))
.attr("y1", format_float(f64::from(y) + 0.25))
.attr("y2", format_float(f64::from(y) - 0.25)),
)
.build()
}
fn draw_triangle(x: u8, y: u8, color: Option<StoneColor>, style: &GobanStyle) -> Element {
let triangle_radius = 0.45;
let points = format!(
"{},{} {},{} {},{}",
x,
format_float(f64::from(y) - triangle_radius),
format_float(f64::from(x) - 0.866 * triangle_radius),
format_float(f64::from(y) + 0.5 * triangle_radius),
format_float(f64::from(x) + 0.866 * triangle_radius),
format_float(f64::from(y) + 0.5 * triangle_radius),
);
Element::builder("g", NAMESPACE)
.attr("stroke", style.markup_color(color))
.attr("fill", "none")
.attr("stroke-width", format_float(style.line_width()))
.append(Element::builder("polygon", NAMESPACE).attr("points", points))
.build()
}
fn draw_circle(x: u8, y: u8, color: Option<StoneColor>, style: &GobanStyle) -> Element {
let radius = 0.25;
Element::builder("g", NAMESPACE)
.attr("stroke", style.markup_color(color))
.attr("fill", "none")
.attr("stroke-width", format_float(style.line_width()))
.append(
Element::builder("circle", NAMESPACE)
.attr("cx", x)
.attr("cy", y)
.attr("r", format_float(radius)),
)
.build()
}
fn draw_square(x: u8, y: u8, color: Option<StoneColor>, style: &GobanStyle) -> Element {
let width = 0.55;
Element::builder("g", NAMESPACE)
.attr("stroke", style.markup_color(color))
.attr("fill", "none")
.attr("stroke-width", format_float(style.line_width()))
.append(
Element::builder("rect", NAMESPACE)
.attr("x", format_float(f64::from(x) - 0.5 * width))
.attr("y", format_float(f64::from(y) - 0.5 * width))
.attr("width", format_float(width))
.attr("height", format_float(width)),
)
.build()
}
fn draw_selected(x: u8, y: u8, color: Option<StoneColor>, style: &GobanStyle) -> Element {
let width = 0.25;
Element::builder("g", NAMESPACE)
.attr("stroke", "none")
.attr("fill", style.selected_color(color))
.attr("stroke-width", style.line_width().to_string())
.append(
Element::builder("rect", NAMESPACE)
.attr("x", (f64::from(x) - 0.5 * width).to_string())
.attr("y", (f64::from(y) - 0.5 * width).to_string())
.attr("width", width.to_string())
.attr("height", width.to_string()),
)
.build()
}
fn dim_square(x: u8, y: u8) -> Element {
Element::builder("g", NAMESPACE)
.attr("stroke", "none")
.attr("fill", "black")
.attr("fill-opacity", "0.5")
.attr("shape-rendering", "crispEdges")
.append(
Element::builder("rect", NAMESPACE)
.attr("x", format_float(f64::from(x) - 0.5))
.attr("y", format_float(f64::from(y) - 0.5))
.attr("width", "1")
.attr("height", "1"),
)
.build()
}
fn draw_label(x: u8, y: u8, text: &str, color: Option<StoneColor>, style: &GobanStyle) -> Element {
let text = text.chars().take(2).collect::<String>();
let text_element = Element::builder("text", NAMESPACE)
.attr("x", x)
.attr("y", y)
.attr("text-anchor", "middle")
.attr("dy", "0.35em")
.attr("fill", style.markup_color(color))
.append(text);
let mut group_builder = Element::builder("g", NAMESPACE);
if color.is_none() {
group_builder = group_builder.append(
Element::builder("rect", NAMESPACE)
.attr("fill", style.background_fill())
.attr("x", format_float(f64::from(x) - 0.4))
.attr("y", format_float(f64::from(y) - 0.4))
.attr("width", "0.8")
.attr("height", "0.8"),
);
}
group_builder.append(text_element).build()
}
fn get_margins(label_sides: &BoardSideSet) -> (f64, f64, f64, f64) {
let top = if label_sides.contains(BoardSide::North) {
LABEL_MARGIN
} else {
0.0
};
let right = if label_sides.contains(BoardSide::East) {
LABEL_MARGIN
} else {
0.0
};
let bottom = if label_sides.contains(BoardSide::South) {
LABEL_MARGIN
} else {
0.0
};
let left = if label_sides.contains(BoardSide::West) {
LABEL_MARGIN
} else {
0.0
};
(top, right, bottom, left)
}
fn format_float(x: f64) -> String {
format!("{:.4}", x)
.trim_end_matches('0')
.trim_end_matches('.')
.to_string()
}