1use std::collections::{HashMap, HashSet};
2use std::ops::Range;
3
4use minidom::Element;
5
6use super::options::BoardSide;
7use super::{board_label_text, BoardSideSet, GobanStyle, MoveNumberOptions, RenderOptions};
8
9use crate::errors::GobanError;
10use crate::goban::{Goban, Stone, StoneColor};
11
12pub static NAMESPACE: &str = "http://www.w3.org/2000/svg";
13
14static BOARD_MARGIN: f64 = 0.64;
15static LABEL_MARGIN: f64 = 0.8;
16static REPEATED_MOVES_MARGIN: f64 = 0.32;
17
18static FONT_FAMILY: &str = "Inter";
19static FONT_SIZE: f64 = 0.45;
20static FONT_WEIGHT: usize = 700;
21
22pub fn render(goban: &Goban, options: &RenderOptions) -> Result<Element, GobanError> {
23 let (x_range, y_range) = options.goban_range.get_ranges(goban, options)?;
24 let width = x_range.end - x_range.start;
25 let height = y_range.end - y_range.start;
26 if !options.label_sides.is_empty() && width > 25 || height > 99 {
27 return Err(GobanError::UnlabellableRange);
28 }
29 let (top_margin, right_margin, bottom_margin, left_margin) = get_margins(&options.label_sides);
30
31 let definitions = {
32 let clip_path = Element::builder("clipPath", NAMESPACE)
33 .attr("id", "board-clip")
34 .append(
35 Element::builder("rect", NAMESPACE)
36 .attr("x", format_float(f64::from(x_range.start) - 0.5))
37 .attr("y", format_float(f64::from(y_range.start) - 0.5))
38 .attr("width", width.to_string())
39 .attr("height", height.to_string())
40 .build(),
41 )
42 .build();
43 Element::builder("defs", NAMESPACE)
44 .append(clip_path)
45 .append_all(options.style.defs()?)
46 .build()
47 };
48 let diagram_width = f64::from(width) - 1.0 + 2.0 * BOARD_MARGIN + left_margin + right_margin;
49
50 let (diagram, diagram_height) = {
51 let board = build_board(goban, options);
52 let board_view = {
53 let board_view_transform = format!(
54 "translate({}, {})",
55 format_float(BOARD_MARGIN + left_margin - f64::from(x_range.start)),
56 format_float(BOARD_MARGIN + top_margin - f64::from(y_range.start))
57 );
58 Element::builder("g", NAMESPACE)
59 .attr("id", "board-view")
60 .attr("transform", board_view_transform)
61 .append(board)
62 .build()
63 };
64
65 let scale = format_float(options.viewbox_width / diagram_width);
66 let transform = format!("scale({scale}, {scale})");
67 let mut diagram_builder = Element::builder("g", NAMESPACE)
68 .attr("id", "diagram")
69 .attr("transform", transform)
70 .append(board_view);
71
72 if !options.label_sides.is_empty() {
73 let goban_size = goban.size();
74 diagram_builder = diagram_builder.append(draw_board_labels(
75 x_range,
76 goban_size.1 - height - y_range.start + 1..goban_size.1 - y_range.start + 1,
77 options,
78 ));
79 }
80
81 let mut diagram_height =
82 f64::from(height) - 1.0 + 2.0 * BOARD_MARGIN + top_margin + bottom_margin;
83 if options.kifu_mode {
84 if let Some((element, element_height)) = draw_repeated_stones(
85 goban,
86 width,
87 diagram_height + REPEATED_MOVES_MARGIN,
88 options,
89 ) {
90 diagram_builder = diagram_builder.append(element);
91 diagram_height += element_height + REPEATED_MOVES_MARGIN * 2.0;
92 }
93 }
94
95 (diagram_builder.build(), diagram_height)
96 };
97
98 let background = Element::builder("rect", NAMESPACE)
99 .attr("fill", options.style.background_fill())
100 .attr("height", "100%")
101 .attr("width", "100%")
102 .attr("x", "0")
103 .attr("y", "0")
104 .build();
105
106 let viewbox_height = options.viewbox_width * diagram_height / diagram_width;
107 let viewbox_attr = format!(
108 "0 0 {} {}",
109 format_float(options.viewbox_width),
110 format_float(viewbox_height)
111 );
112 let svg = Element::builder("svg", NAMESPACE)
113 .attr("viewBox", viewbox_attr)
114 .attr("width", options.viewbox_width.to_string())
115 .attr("font-size", FONT_SIZE.to_string())
116 .attr("font-family", FONT_FAMILY)
117 .attr("font-weight", FONT_WEIGHT)
118 .append(definitions)
119 .append(background)
120 .append(diagram)
121 .build();
122 Ok(svg)
123}
124
125fn build_board(goban: &Goban, options: &RenderOptions) -> Element {
127 let mut group_builder = Element::builder("g", NAMESPACE)
128 .attr("id", "goban")
129 .attr("clip-path", "url(#board-clip)")
130 .append(build_board_lines_group(goban, options))
131 .append(build_stones_group(goban, options));
132
133 let move_numbers = get_move_numbers(goban, options);
134 let no_markup_points: HashSet<(u8, u8)> = move_numbers
135 .iter()
136 .map(|(_, stone)| (stone.x, stone.y))
137 .collect();
138 if let Some(move_number_options) = &options.move_number_options {
139 group_builder = group_builder.append(build_move_numbers_group(
140 goban,
141 options,
142 move_number_options,
143 &move_numbers,
144 ));
145 }
146 if options.draw_marks {
147 group_builder = group_builder.append(build_marks_group(goban, options, &no_markup_points));
148 }
149 if options.draw_triangles {
150 group_builder =
151 group_builder.append(build_triangles_group(goban, options, &no_markup_points));
152 }
153 if options.draw_circles {
154 group_builder =
155 group_builder.append(build_circles_group(goban, options, &no_markup_points));
156 }
157 if options.draw_squares {
158 group_builder =
159 group_builder.append(build_squares_group(goban, options, &no_markup_points));
160 }
161 if options.draw_selected {
162 group_builder =
163 group_builder.append(build_selected_group(goban, options, &no_markup_points));
164 }
165 if options.draw_dimmed {
166 group_builder = group_builder.append(build_dimmed_group(goban, options));
167 }
168 if options.draw_labels {
169 group_builder = group_builder.append(build_label_group(goban, options, &no_markup_points));
170 }
171 if options.draw_lines {
172 group_builder = group_builder.append(build_line_group(goban, options));
173 }
174 if options.draw_arrows {
175 group_builder = group_builder.append(build_arrow_group(goban, options));
176 }
177
178 group_builder.build()
179}
180
181fn build_board_lines_group(goban: &Goban, options: &RenderOptions) -> Element {
182 let mut group_builder = Element::builder("g", NAMESPACE)
183 .attr("id", "lines")
184 .attr("stroke", options.style.line_color())
185 .attr("stroke-width", format_float(options.style.line_width()))
186 .attr("stroke-linecap", "square");
187
188 let goban_size = goban.size();
190 for x in 0..goban_size.0 as usize {
191 group_builder = group_builder.append(
192 Element::builder("line", NAMESPACE)
193 .attr("x1", x.to_string())
194 .attr("y1", "0")
195 .attr("x2", x.to_string())
196 .attr("y2", (goban_size.1 - 1).to_string()),
197 );
198 }
199 for y in 0..goban_size.1 as usize {
200 group_builder = group_builder.append(
201 Element::builder("line", NAMESPACE)
202 .attr("x1", 0.to_string())
203 .attr("y1", y.to_string())
204 .attr("x2", (goban_size.0 - 1).to_string())
205 .attr("y2", y.to_string()),
206 );
207 }
208
209 let hoshi_radius = options.style.hoshi_radius();
211 let mut hoshi = Element::builder("g", NAMESPACE)
212 .attr("id", "hoshi")
213 .attr("stroke", "none")
214 .attr("fill", options.style.line_color());
215 for (x, y) in goban.hoshi_points() {
216 hoshi = hoshi.append(
217 Element::builder("circle", NAMESPACE)
218 .attr("cx", x.to_string())
219 .attr("cy", y.to_string())
220 .attr("r", format_float(hoshi_radius)),
221 );
222 }
223 group_builder.append(hoshi).build()
224}
225
226fn build_stones_group(goban: &Goban, options: &RenderOptions) -> Element {
227 let mut group_builder = Element::builder("g", NAMESPACE)
228 .attr("id", "stones")
229 .attr("stroke", "none");
230 let mut stones: Vec<Stone> = if options.kifu_mode {
231 let mut stones: HashMap<(u8, u8), Stone> = HashMap::new();
233 let mut numbered_stones: HashMap<(u8, u8), Stone> = HashMap::new();
234 for (n, stone) in goban.moves() {
235 if n >= options.move_number_options.unwrap().start {
236 numbered_stones.entry((stone.x, stone.y)).or_insert(stone);
237 }
238 stones.insert((stone.x, stone.y), stone);
239 }
240 for (key, stone) in numbered_stones {
241 stones.insert(key, stone);
242 }
243 stones.into_values().collect()
244 } else {
245 goban.stones().collect()
246 };
247 stones.sort_by_key(|stone| (stone.y, stone.x));
248 for stone in stones {
249 group_builder = group_builder.append(draw_stone(stone, &options.style));
250 }
251 group_builder.build()
252}
253
254fn build_move_numbers_group(
255 goban: &Goban,
256 options: &RenderOptions,
257 move_number_options: &MoveNumberOptions,
258 move_numbers: &[(u64, Stone)],
259) -> Element {
260 let mut group_builder = Element::builder("g", NAMESPACE)
261 .attr("id", "move-numbers")
262 .attr("text-anchor", "middle");
263 for (n, stone) in move_numbers {
264 let stone_color = if options.kifu_mode {
265 Some(stone.color)
267 } else {
268 goban.stone_color(stone.x, stone.y)
270 };
271 let move_number = n + move_number_options.count_from - move_number_options.start;
272 group_builder = group_builder.append(draw_move_number(
273 stone.x,
274 stone.y,
275 move_number,
276 stone_color,
277 &options.style,
278 ));
279 }
280 group_builder.build()
281}
282
283fn get_move_numbers(goban: &Goban, options: &RenderOptions) -> Vec<(u64, Stone)> {
284 let move_number_options = match options.move_number_options {
285 Some(move_number_options) => move_number_options,
286 None => return Vec::new(),
287 };
288 let numbered_moves = goban
289 .moves()
290 .skip_while(|&(n, _)| n < move_number_options.start)
291 .take_while(|&(n, _)| move_number_options.end.map(|end| n <= end).unwrap_or(true));
292 let mut move_numbers: HashMap<(u8, u8), (u64, Stone)> = HashMap::new();
293 for (n, stone) in numbered_moves {
294 if options.kifu_mode {
295 move_numbers.entry((stone.x, stone.y)).or_insert((n, stone));
297 } else {
298 move_numbers.insert((stone.x, stone.y), (n, stone));
300 }
301 }
302 let mut move_numbers: Vec<(u64, Stone)> = move_numbers.values().copied().collect();
303 move_numbers.sort_unstable_by_key(|(n, _)| *n);
304 move_numbers
305}
306
307fn build_marks_group(
308 goban: &Goban,
309 options: &RenderOptions,
310 no_markup_points: &HashSet<(u8, u8)>,
311) -> Element {
312 let mut group_builder = Element::builder("g", NAMESPACE).attr("id", "markup-marks");
313 let mut marks: Vec<_> = goban.marks().collect();
314 marks.sort_unstable();
315 for point in marks.iter().filter(|p| !no_markup_points.contains(p)) {
316 let stone_color = goban.stone_color(point.0, point.1);
317 group_builder =
318 group_builder.append(draw_mark(point.0, point.1, stone_color, &options.style));
319 }
320 group_builder.build()
321}
322
323fn build_triangles_group(
324 goban: &Goban,
325 options: &RenderOptions,
326 no_markup_points: &HashSet<(u8, u8)>,
327) -> Element {
328 let mut group_builder = Element::builder("g", NAMESPACE).attr("id", "markup-triangles");
329 let mut triangles: Vec<_> = goban.triangles().collect();
330 triangles.sort_unstable();
331 for point in triangles.iter().filter(|p| !no_markup_points.contains(p)) {
332 let stone_color = goban.stone_color(point.0, point.1);
333 group_builder =
334 group_builder.append(draw_triangle(point.0, point.1, stone_color, &options.style));
335 }
336 group_builder.build()
337}
338
339fn build_circles_group(
340 goban: &Goban,
341 options: &RenderOptions,
342 no_markup_points: &HashSet<(u8, u8)>,
343) -> Element {
344 let mut group_builder = Element::builder("g", NAMESPACE).attr("id", "markup-circles");
345 let mut circles: Vec<_> = goban.circles().collect();
346 circles.sort_unstable();
347 for point in circles.iter().filter(|p| !no_markup_points.contains(p)) {
348 let stone_color = goban.stone_color(point.0, point.1);
349 group_builder =
350 group_builder.append(draw_circle(point.0, point.1, stone_color, &options.style));
351 }
352 group_builder.build()
353}
354
355fn build_squares_group(
356 goban: &Goban,
357 options: &RenderOptions,
358 no_markup_points: &HashSet<(u8, u8)>,
359) -> Element {
360 let mut group_builder = Element::builder("g", NAMESPACE).attr("id", "markup-squares");
361 let mut squares: Vec<_> = goban.squares().collect();
362 squares.sort_unstable();
363 for point in squares.iter().filter(|p| !no_markup_points.contains(p)) {
364 let stone_color = goban.stone_color(point.0, point.1);
365 group_builder =
366 group_builder.append(draw_square(point.0, point.1, stone_color, &options.style));
367 }
368 group_builder.build()
369}
370
371fn build_selected_group(
372 goban: &Goban,
373 options: &RenderOptions,
374 no_markup_points: &HashSet<(u8, u8)>,
375) -> Element {
376 let mut group_builder = Element::builder("g", NAMESPACE).attr("id", "markup-selected");
377 let mut selected: Vec<_> = goban.selected().collect();
378 selected.sort_unstable();
379 for point in selected.iter().filter(|p| !no_markup_points.contains(p)) {
380 let stone_color = goban.stone_color(point.0, point.1);
381 group_builder =
382 group_builder.append(draw_selected(point.0, point.1, stone_color, &options.style));
383 }
384 group_builder.build()
385}
386
387fn build_dimmed_group(goban: &Goban, _options: &RenderOptions) -> Element {
388 let mut group_builder = Element::builder("g", NAMESPACE).attr("id", "markup-dimmed");
389 let mut dimmed: Vec<_> = goban.dimmed().collect();
390 dimmed.sort_unstable();
391 for point in dimmed {
392 group_builder = group_builder.append(dim_square(point.0, point.1));
393 }
394 group_builder.build()
395}
396
397fn build_label_group(
398 goban: &Goban,
399 options: &RenderOptions,
400 no_markup_points: &HashSet<(u8, u8)>,
401) -> Element {
402 let mut group_builder = Element::builder("g", NAMESPACE).attr("id", "markup-labels");
403 let mut labels: Vec<_> = goban.labels().collect();
404 labels.sort_unstable();
405 for (point, text) in labels.iter().filter(|(p, _)| !no_markup_points.contains(p)) {
406 let stone_color = goban.stone_color(point.0, point.1);
407 group_builder = group_builder.append(draw_label(
408 point.0,
409 point.1,
410 text,
411 stone_color,
412 &options.style,
413 ));
414 }
415 group_builder.build()
416}
417
418fn build_line_group(goban: &Goban, options: &RenderOptions) -> Element {
419 let mut group_builder = Element::builder("g", NAMESPACE)
420 .attr("id", "markup-lines")
421 .attr("stroke", "black")
422 .attr("stroke-width", format_float(options.style.line_width()))
423 .attr("marker-start", "url(#linehead)")
424 .attr("marker-end", "url(#linehead)");
425 let mut lines: Vec<_> = goban.lines().collect();
426 lines.sort_unstable();
427 for (p1, p2) in lines {
428 group_builder = group_builder.append(
429 Element::builder("line", NAMESPACE)
430 .attr("x1", p1.0)
431 .attr("x2", p2.0)
432 .attr("y1", p1.1)
433 .attr("y2", p2.1),
434 );
435 }
436 group_builder.build()
437}
438
439fn build_arrow_group(goban: &Goban, options: &RenderOptions) -> Element {
440 let mut group_builder = Element::builder("g", NAMESPACE)
441 .attr("id", "markup-arrows")
442 .attr("stroke", "black")
443 .attr("stroke-width", format_float(options.style.line_width()))
444 .attr("marker-end", "url(#arrowhead)");
445 let mut arrows: Vec<_> = goban.arrows().collect();
446 arrows.sort_unstable();
447 for (p1, p2) in arrows {
448 group_builder = group_builder.append(
449 Element::builder("line", NAMESPACE)
450 .attr("x1", p1.0)
451 .attr("x2", p2.0)
452 .attr("y1", p1.1)
453 .attr("y2", p2.1),
454 );
455 }
456
457 group_builder.build()
458}
459
460fn draw_board_labels(x_range: Range<u8>, y_range: Range<u8>, options: &RenderOptions) -> Element {
465 let (top_margin, _, _, left_margin) = get_margins(&options.label_sides);
466 let transform = format!(
467 "translate({}, {})",
468 format_float(left_margin),
469 format_float(top_margin)
470 );
471 let mut group_builder = Element::builder("g", NAMESPACE)
472 .attr("id", "board-labels")
473 .attr("fill", options.style.label_color())
474 .attr("transform", transform);
475
476 if options.label_sides.contains(BoardSide::North) {
477 let mut builder = Element::builder("g", NAMESPACE).attr("text-anchor", "middle");
478 let start = x_range.start;
479 for x in x_range.clone() {
480 builder = builder.append(
481 Element::builder("text", NAMESPACE)
482 .attr("x", format_float(f64::from(x - start) + BOARD_MARGIN))
483 .attr("y", "0")
484 .append(board_label_text(x))
485 .build(),
486 );
487 }
488 group_builder = group_builder.append(builder);
489 };
490 if options.label_sides.contains(BoardSide::West) {
491 let mut builder = Element::builder("g", NAMESPACE).attr("text-anchor", "end");
492 let end = y_range.end;
493 for y in y_range.clone() {
494 builder = builder.append(
495 Element::builder("text", NAMESPACE)
496 .attr("x", "0")
497 .attr("y", format_float(f64::from(end - y - 1) + BOARD_MARGIN))
498 .attr("dy", "0.35em")
499 .append(y.to_string())
500 .build(),
501 );
502 }
503 group_builder = group_builder.append(builder);
504 };
505 if options.label_sides.contains(BoardSide::South) {
506 let mut builder = Element::builder("g", NAMESPACE).attr("text-anchor", "middle");
507 let start = x_range.start;
508 let y = f64::from(y_range.end - y_range.start + 1) - BOARD_MARGIN;
509 for x in x_range.clone() {
510 builder = builder.append(
511 Element::builder("text", NAMESPACE)
512 .attr("x", format_float(f64::from(x - start) + BOARD_MARGIN))
513 .attr("y", format_float(y))
514 .attr("alignment-baseline", "hanging")
515 .append(board_label_text(x))
516 .build(),
517 );
518 }
519 group_builder = group_builder.append(builder);
520 };
521 if options.label_sides.contains(BoardSide::East) {
522 let mut builder = Element::builder("g", NAMESPACE).attr("text-anchor", "start");
523 let end = y_range.end;
524 let x = f64::from(x_range.end - x_range.start + 1) - BOARD_MARGIN;
525 for y in y_range {
526 builder = builder.append(
527 Element::builder("text", NAMESPACE)
528 .attr("x", format_float(x))
529 .attr("y", format_float(f64::from(end - y - 1) + BOARD_MARGIN))
530 .attr("dy", "0.35em")
531 .append(y.to_string())
532 .build(),
533 );
534 }
535 group_builder = group_builder.append(builder);
536 };
537
538 group_builder.build()
539}
540
541fn draw_repeated_stones(
542 goban: &Goban,
543 width: u8,
544 diagram_height: f64,
545 options: &RenderOptions,
546) -> Option<(Element, f64)> {
547 let entry_padding = 0.25;
548 let entry_width = 2.43;
549 let entry_height = 0.4;
550 let width = f64::from(width);
551 let (_, _, _, left_margin) = get_margins(&options.label_sides);
552 let columns = ((width - 1.0 - (2.0 * entry_padding)) / entry_width).floor() as usize;
553 let x = BOARD_MARGIN
554 + left_margin
555 + entry_padding
556 + (width - 1.0 - 2.0 * entry_padding - entry_width * f64::from(columns as u32)) / 2.0;
557 let y = diagram_height + entry_padding + entry_height;
558 let mut text_builder = Element::builder("text", NAMESPACE)
559 .attr("y", format_float(y))
560 .attr("font-size", format_float(entry_height))
561 .attr("fill", options.style.line_color()); let move_number_options = options.move_number_options.unwrap();
563 let repeated_moves: Vec<(u64, u64)> = {
564 let mut repeated_moves = Vec::new();
565 let mut seen_moves: HashMap<(u8, u8), u64> = HashMap::new();
566 for (n, stone) in goban.moves() {
567 match seen_moves.entry((stone.x, stone.y)) {
568 std::collections::hash_map::Entry::Occupied(mut entry) => {
569 if *entry.get() < move_number_options.start {
570 entry.insert(n);
571 }
572 let value = *entry.get();
573 let in_range = value >= move_number_options.start
574 && move_number_options
575 .end
576 .map(|end| value <= end)
577 .unwrap_or(true);
578 if n > value && in_range {
579 repeated_moves.push((
580 n + move_number_options.count_from - move_number_options.start,
581 value + move_number_options.count_from - move_number_options.start,
582 ));
583 }
584 }
585 std::collections::hash_map::Entry::Vacant(entry) => {
586 entry.insert(n);
587 }
588 }
589 }
590 repeated_moves.sort_unstable();
591 repeated_moves
592 };
593 if repeated_moves.is_empty() {
594 return None;
595 }
596 let rows = (f64::from(repeated_moves.len() as u32) / f64::from(columns as u32)).ceil();
597 for (i, (move_num, original)) in repeated_moves.iter().enumerate() {
598 let column = f64::from((i % columns) as u32);
599 let mut tspan_builder = Element::builder("tspan", NAMESPACE)
600 .append(format!("{move_num}→{original}"))
601 .attr("x", format_float(x + entry_width * column));
602 if i % columns == 0 && i != 0 {
603 tspan_builder = tspan_builder.attr("dy", format_float(entry_height));
604 }
605 text_builder = text_builder.append(tspan_builder);
606 }
607 let rect_height = 2.0 * entry_padding + entry_height * rows + options.style.line_width() * 2.0;
608 let group = Element::builder("g", NAMESPACE)
609 .attr("id", "repeated-stones")
610 .append(
611 Element::builder("rect", NAMESPACE)
612 .attr("fill", "white")
613 .attr("stroke", options.style.line_color())
614 .attr("stroke-width", format_float(options.style.line_width()))
615 .attr("x", format_float(BOARD_MARGIN + left_margin))
616 .attr("y", format_float(diagram_height))
617 .attr("width", format_float(width - 1.0))
618 .attr("height", format_float(rect_height)),
619 )
620 .append(text_builder)
621 .build();
622
623 Some((group, rect_height))
624}
625
626fn draw_stone(stone: Stone, style: &GobanStyle) -> Element {
627 let mut circle_builder = Element::builder("circle", NAMESPACE)
628 .attr("cx", stone.x)
629 .attr("cy", stone.y)
630 .attr("r", "0.48");
631 if let Some(stroke) = style.stone_stroke(stone.color) {
632 circle_builder = circle_builder
633 .attr("stroke", stroke)
634 .attr("stroke-width", format_float(style.line_width()))
635 }
636 if let Some(fill) = style.stone_fill(stone.color) {
637 circle_builder = circle_builder.attr("fill", fill);
638 }
639 circle_builder.build()
640}
641
642fn draw_move_number(
643 x: u8,
644 y: u8,
645 n: u64,
646 color: Option<StoneColor>,
647 style: &GobanStyle,
648) -> Element {
649 let text_element = Element::builder("text", NAMESPACE)
651 .attr("x", x)
652 .attr("y", y)
653 .attr("dy", "0.35em")
654 .attr("fill", style.markup_color(color))
655 .append(n.to_string());
656 let mut group_builder = Element::builder("g", NAMESPACE);
657 if color.is_none() {
658 group_builder = group_builder.append(
659 Element::builder("rect", NAMESPACE)
660 .attr("fill", style.background_fill())
661 .attr("x", format_float(f64::from(x) - 0.4))
662 .attr("y", format_float(f64::from(y) - 0.4))
663 .attr("width", "0.8")
664 .attr("height", "0.8"),
665 );
666 }
667
668 group_builder.append(text_element).build()
669}
670
671fn draw_mark(x: u8, y: u8, color: Option<StoneColor>, style: &GobanStyle) -> Element {
672 Element::builder("g", NAMESPACE)
673 .attr("stroke", style.markup_color(color))
674 .attr("stroke-width", style.markup_stroke_width().to_string())
675 .append(
676 Element::builder("line", NAMESPACE)
677 .attr("x1", format_float(f64::from(x) - 0.25))
678 .attr("x2", format_float(f64::from(x) + 0.25))
679 .attr("y1", format_float(f64::from(y) - 0.25))
680 .attr("y2", format_float(f64::from(y) + 0.25)),
681 )
682 .append(
683 Element::builder("line", NAMESPACE)
684 .attr("x1", format_float(f64::from(x) - 0.25))
685 .attr("x2", format_float(f64::from(x) + 0.25))
686 .attr("y1", format_float(f64::from(y) + 0.25))
687 .attr("y2", format_float(f64::from(y) - 0.25)),
688 )
689 .build()
690}
691
692fn draw_triangle(x: u8, y: u8, color: Option<StoneColor>, style: &GobanStyle) -> Element {
693 let triangle_radius = 0.45;
694 let points = format!(
695 "{},{} {},{} {},{}",
696 x,
697 format_float(f64::from(y) - triangle_radius),
698 format_float(f64::from(x) - 0.866 * triangle_radius),
699 format_float(f64::from(y) + 0.5 * triangle_radius),
700 format_float(f64::from(x) + 0.866 * triangle_radius),
701 format_float(f64::from(y) + 0.5 * triangle_radius),
702 );
703 Element::builder("g", NAMESPACE)
704 .attr("stroke", style.markup_color(color))
705 .attr("fill", "none")
706 .attr("stroke-width", format_float(style.line_width()))
707 .append(Element::builder("polygon", NAMESPACE).attr("points", points))
708 .build()
709}
710
711fn draw_circle(x: u8, y: u8, color: Option<StoneColor>, style: &GobanStyle) -> Element {
712 let radius = 0.25;
713 Element::builder("g", NAMESPACE)
714 .attr("stroke", style.markup_color(color))
715 .attr("fill", "none")
716 .attr("stroke-width", format_float(style.line_width()))
717 .append(
718 Element::builder("circle", NAMESPACE)
719 .attr("cx", x)
720 .attr("cy", y)
721 .attr("r", format_float(radius)),
722 )
723 .build()
724}
725
726fn draw_square(x: u8, y: u8, color: Option<StoneColor>, style: &GobanStyle) -> Element {
727 let width = 0.55;
728 Element::builder("g", NAMESPACE)
729 .attr("stroke", style.markup_color(color))
730 .attr("fill", "none")
731 .attr("stroke-width", format_float(style.line_width()))
732 .append(
733 Element::builder("rect", NAMESPACE)
734 .attr("x", format_float(f64::from(x) - 0.5 * width))
735 .attr("y", format_float(f64::from(y) - 0.5 * width))
736 .attr("width", format_float(width))
737 .attr("height", format_float(width)),
738 )
739 .build()
740}
741
742fn draw_selected(x: u8, y: u8, color: Option<StoneColor>, style: &GobanStyle) -> Element {
743 let width = 0.25;
744 Element::builder("g", NAMESPACE)
745 .attr("stroke", "none")
746 .attr("fill", style.selected_color(color))
747 .attr("stroke-width", style.line_width().to_string())
748 .append(
749 Element::builder("rect", NAMESPACE)
750 .attr("x", (f64::from(x) - 0.5 * width).to_string())
751 .attr("y", (f64::from(y) - 0.5 * width).to_string())
752 .attr("width", width.to_string())
753 .attr("height", width.to_string()),
754 )
755 .build()
756}
757
758fn dim_square(x: u8, y: u8) -> Element {
759 Element::builder("g", NAMESPACE)
760 .attr("stroke", "none")
761 .attr("fill", "black")
762 .attr("fill-opacity", "0.5")
763 .attr("shape-rendering", "crispEdges")
764 .append(
765 Element::builder("rect", NAMESPACE)
766 .attr("x", format_float(f64::from(x) - 0.5))
767 .attr("y", format_float(f64::from(y) - 0.5))
768 .attr("width", "1")
769 .attr("height", "1"),
770 )
771 .build()
772}
773
774fn draw_label(x: u8, y: u8, text: &str, color: Option<StoneColor>, style: &GobanStyle) -> Element {
775 let text = text.chars().take(2).collect::<String>();
776 let text_element = Element::builder("text", NAMESPACE)
777 .attr("x", x)
778 .attr("y", y)
779 .attr("text-anchor", "middle")
780 .attr("dy", "0.35em")
781 .attr("fill", style.markup_color(color))
782 .append(text);
783 let mut group_builder = Element::builder("g", NAMESPACE);
784 if color.is_none() {
785 group_builder = group_builder.append(
786 Element::builder("rect", NAMESPACE)
787 .attr("fill", style.background_fill())
788 .attr("x", format_float(f64::from(x) - 0.4))
789 .attr("y", format_float(f64::from(y) - 0.4))
790 .attr("width", "0.8")
791 .attr("height", "0.8"),
792 );
793 }
794
795 group_builder.append(text_element).build()
796}
797
798fn get_margins(label_sides: &BoardSideSet) -> (f64, f64, f64, f64) {
799 let top = if label_sides.contains(BoardSide::North) {
800 LABEL_MARGIN
801 } else {
802 0.0
803 };
804 let right = if label_sides.contains(BoardSide::East) {
805 LABEL_MARGIN
806 } else {
807 0.0
808 };
809 let bottom = if label_sides.contains(BoardSide::South) {
810 LABEL_MARGIN
811 } else {
812 0.0
813 };
814 let left = if label_sides.contains(BoardSide::West) {
815 LABEL_MARGIN
816 } else {
817 0.0
818 };
819 (top, right, bottom, left)
820}
821
822fn format_float(x: f64) -> String {
823 format!("{x:.4}")
824 .trim_end_matches('0')
825 .trim_end_matches('.')
826 .to_string()
827}