Skip to content

Commit 3ebe265

Browse files
committed
Add initial SVG renderer for thermographs
1 parent 23c3b73 commit 3ebe265

File tree

8 files changed

+268
-61
lines changed

8 files changed

+268
-61
lines changed

cgt-py/src/canonical_form.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,8 @@ impl PyCanonicalForm {
7070
self.inner.temperature().into()
7171
}
7272

73-
// TODO: Convert to svg
7473
fn thermograph(&self) -> String {
75-
self.inner.thermograph().to_string()
74+
// format!("{:?}", self.inner.thermograph())
75+
self.inner.thermograph().to_svg()
7676
}
7777
}

src/drawing.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
//! Drawing module
2+
3+
pub mod svg;

src/drawing/svg.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
//! Simple SVG immediate drawing utilities
2+
3+
use std::fmt::{self, Write};
4+
5+
/// SVG renderer
6+
pub struct Svg;
7+
8+
impl Svg {
9+
/// Create new SVG
10+
pub fn new<W>(
11+
w: &mut W,
12+
width: u32,
13+
height: u32,
14+
cont: impl FnOnce(&mut W) -> fmt::Result,
15+
) -> fmt::Result
16+
where
17+
W: Write,
18+
{
19+
write!(w, "<svg width=\"{}\" height=\"{}\">", width, height)?;
20+
cont(w)?;
21+
write!(w, "</svg>")
22+
}
23+
24+
/// Create [group element](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/g)
25+
pub fn g<W>(w: &mut W, stroke: &str, cont: impl FnOnce(&mut W) -> fmt::Result) -> fmt::Result
26+
where
27+
W: Write,
28+
{
29+
write!(w, "<g stroke=\"{}\">", stroke)?;
30+
cont(w)?;
31+
write!(w, "</g>")
32+
}
33+
34+
/// Create [line element](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/line)
35+
pub fn line<W>(w: &mut W, x1: i32, y1: i32, x2: i32, y2: i32, stroke_width: u32) -> fmt::Result
36+
where
37+
W: Write,
38+
{
39+
write!(
40+
w,
41+
"<line x1=\"{}\" y1=\"{}\" x2=\"{}\" y2=\"{}\" style=\"stroke-width:{};\"/>",
42+
x1, y1, x2, y2, stroke_width
43+
)
44+
}
45+
46+
/// Create [rectangle element](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/rect)
47+
pub fn rect<W>(w: &mut W, x: i32, y: i32, width: u32, height: u32, fill: &str) -> fmt::Result
48+
where
49+
W: Write,
50+
{
51+
write!(
52+
w,
53+
"<rect x=\"{}\" y=\"{}\" width=\"{}\" height=\"{}\" style=\"fill:{};\"/>",
54+
x, y, width, height, fill,
55+
)
56+
}
57+
58+
/// Create [text element](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/text)
59+
pub fn text<W>(w: &mut W, x: i32, y: i32, text: &str) -> fmt::Result
60+
where
61+
W: Write,
62+
{
63+
write!(w, "<text x=\"{}\" y=\"{}\">{}</text>", x, y, text,)
64+
}
65+
}

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
)
2424
)]
2525

26+
pub mod drawing;
2627
pub mod graph;
2728
pub mod loopy;
2829
pub mod numeric;

src/numeric/rational.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,15 @@ impl Rational {
5555
Err(_) => Ok((input, Self::from(numerator))),
5656
}
5757
}
58+
59+
/// Rounding towards zero if finite
60+
pub fn try_round(&self) -> Option<i64> {
61+
match self {
62+
Self::NegativeInfinity => None,
63+
Self::Value(val) => Some(val.to_integer()),
64+
Self::PositiveInfinity => None,
65+
}
66+
}
5867
}
5968

6069
impl From<Rational64> for Rational {

src/short/partizan/games/domineering.rs

Lines changed: 45 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//! horizontal dominoes.
33
44
extern crate alloc;
5-
use crate::short::partizan::partizan_game::PartizanGame;
5+
use crate::{drawing::svg::Svg, short::partizan::partizan_game::PartizanGame};
66
use alloc::collections::vec_deque::VecDeque;
77
use std::{fmt::Display, str::FromStr};
88

@@ -342,67 +342,57 @@ impl Domineering {
342342

343343
/// Output SVG string with domineering grid
344344
pub fn to_svg(&self) -> String {
345-
use std::fmt::Write;
346-
let mut buf = String::new();
347-
345+
// Chosen arbitrarily
348346
let tile_size = 48;
349347
let grid_width = 4;
350-
let offset = grid_width / 2;
351348

352-
let svg_width = self.width() * tile_size + grid_width;
353-
let svg_height = self.height() * tile_size + grid_width;
354-
355-
write!(
356-
buf,
357-
"<svg width=\"{}\" height=\"{}\">",
358-
svg_width, svg_height,
359-
)
360-
.unwrap();
349+
let offset = grid_width / 2;
350+
let svg_width = self.width() as u32 * tile_size + grid_width;
351+
let svg_height = self.height() as u32 * tile_size + grid_width;
361352

362-
for y in 0..self.height() {
363-
for x in 0..self.width() {
364-
let fill = if self.at(x, y) { "gray" } else { "white" };
365-
366-
write!(
367-
buf,
368-
"<rect x=\"{}\" y=\"{}\" width=\"{}\" height=\"{}\" style=\"fill:{};\"/>",
369-
x * tile_size + offset,
370-
y * tile_size + offset,
371-
tile_size,
372-
tile_size,
373-
fill,
374-
)
375-
.unwrap();
353+
let mut buf = String::new();
354+
Svg::new(&mut buf, svg_width, svg_width, |buf| {
355+
for y in 0..self.height() {
356+
for x in 0..self.width() {
357+
let fill = if self.at(x, y) { "gray" } else { "white" };
358+
Svg::rect(
359+
buf,
360+
(x as u32 * tile_size + offset) as i32,
361+
(y as u32 * tile_size + offset) as i32,
362+
tile_size,
363+
tile_size,
364+
fill,
365+
)?;
366+
}
376367
}
377-
}
378368

379-
write!(buf, "<g stroke=\"black\">",).unwrap();
380-
for y in 0..(self.height() + 1) {
381-
write!(
382-
buf,
383-
"<line x1=\"{}\" y1=\"{}\" x2=\"{}\" y2=\"{}\" style=\"stroke-width:{};\"/>",
384-
0,
385-
y * tile_size + offset,
386-
svg_width,
387-
y * tile_size + offset,
388-
grid_width
389-
)
390-
.unwrap();
391-
}
392-
for x in 0..(self.width() + 1) {
393-
write!(
394-
buf,
395-
"<line x1=\"{}\" y1=\"{}\" x2=\"{}\" y2=\"{}\" style=\"stroke-width:{};\"/>",
396-
x * tile_size + offset,
397-
0,
398-
x * tile_size + offset,
399-
svg_height,
400-
grid_width
401-
)
402-
.unwrap();
403-
}
369+
Svg::g(buf, "black", |buf| {
370+
for y in 0..(self.height() + 1) {
371+
Svg::line(
372+
buf,
373+
0,
374+
(y as u32 * tile_size + offset) as i32,
375+
svg_width as i32,
376+
(y as u32 * tile_size + offset) as i32,
377+
grid_width,
378+
)?;
379+
}
380+
381+
for x in 0..(self.width() + 1) {
382+
Svg::line(
383+
buf,
384+
(x as u32 * tile_size + offset) as i32,
385+
0,
386+
(x as u32 * tile_size + offset) as i32,
387+
svg_height as i32,
388+
grid_width,
389+
)?;
390+
}
404391

405-
write!(buf, "</g></svg>",).unwrap();
392+
Ok(())
393+
})
394+
})
395+
.unwrap();
406396

407397
buf
408398
}

src/short/partizan/thermograph.rs

Lines changed: 142 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
//! Thermograph constructed from scaffolds with support for subzero thermography
22
3-
use crate::{display, numeric::rational::Rational, short::partizan::trajectory::Trajectory};
4-
use std::cmp::Ordering;
5-
use std::fmt::Display;
3+
use crate::{
4+
display, drawing::svg::Svg, numeric::rational::Rational,
5+
short::partizan::trajectory::Trajectory,
6+
};
7+
use ahash::{HashSet, HashSetExt};
8+
use core::fmt;
9+
use std::{cmp::Ordering, fmt::Display, iter::once};
610

711
/// See [thermograph](self) header
812
#[derive(Debug, Hash, Clone, PartialEq, Eq)]
@@ -455,6 +459,141 @@ impl Thermograph {
455459
right_wall,
456460
}
457461
}
462+
463+
/// Render thermograph as SVG image
464+
pub fn to_svg(&self) -> String {
465+
fn rescale(x: i64, in_min: i64, in_max: i64, out_min: i64, out_max: i64) -> i64 {
466+
(x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min
467+
}
468+
469+
// Chosen arbitrarily, may be customizable in the future
470+
let svg_width = 450;
471+
let svg_height = 300;
472+
let mast_arrow_len = Rational::from(3);
473+
let axis_weight = 1;
474+
let thermograph_line_weight = 3;
475+
let padding = 32;
476+
477+
let thermograph_x_min = self
478+
.right_wall
479+
.value_at(Rational::from(-1))
480+
.try_round()
481+
.unwrap();
482+
let thermograph_x_max = self
483+
.left_wall
484+
.value_at(Rational::from(-1))
485+
.try_round()
486+
.unwrap();
487+
488+
let thermograph_y_min = 0;
489+
let thermograph_y_max = (self.get_temperature() + mast_arrow_len)
490+
.try_round()
491+
.unwrap();
492+
493+
let x_axis_location = (svg_height as f32 * 0.9) as i32;
494+
let y_axis_location = rescale(
495+
0,
496+
thermograph_x_min,
497+
thermograph_x_max,
498+
svg_width as i64 - padding,
499+
padding,
500+
) as i32;
501+
502+
let from_thermograph_horizontal = |value| {
503+
rescale(
504+
value,
505+
thermograph_x_min,
506+
thermograph_x_max,
507+
svg_width as i64 - padding,
508+
padding,
509+
)
510+
};
511+
let from_thermograph_vertical = |value| {
512+
rescale(
513+
value,
514+
thermograph_y_min,
515+
thermograph_y_max,
516+
x_axis_location as i64,
517+
padding,
518+
)
519+
};
520+
521+
let draw_scaffold = |w: &mut String,
522+
seen: &mut HashSet<(i64, i64)>,
523+
trajectory: &Trajectory|
524+
-> fmt::Result {
525+
let mut previous = None;
526+
527+
let y_points = once(trajectory.mast_x_intercept() + mast_arrow_len).chain(
528+
trajectory
529+
.critical_points
530+
.iter()
531+
.copied()
532+
.chain(once(Rational::from(-1))),
533+
);
534+
535+
for point_y in y_points {
536+
let point_x = trajectory.value_at(point_y);
537+
538+
let image_x = from_thermograph_horizontal(point_x.try_round().unwrap());
539+
let image_y = from_thermograph_vertical(point_y.try_round().unwrap());
540+
541+
if !seen.contains(&(image_x, image_y)) {
542+
// TODO: Make it less ugly, maybe move values to axis rather than having them on
543+
// critical points
544+
Svg::text(
545+
w,
546+
image_x as i32,
547+
image_y as i32,
548+
&format!("({}, {})", point_x, point_y),
549+
)?;
550+
seen.insert((image_x, image_y));
551+
}
552+
553+
if let Some((previous_x, previous_y)) = previous {
554+
Svg::line(
555+
w,
556+
previous_x as i32,
557+
previous_y as i32,
558+
image_x as i32,
559+
image_y as i32,
560+
thermograph_line_weight,
561+
)?;
562+
}
563+
564+
previous = Some((image_x, image_y));
565+
}
566+
Ok(())
567+
};
568+
569+
let mut buf = String::new();
570+
Svg::new(&mut buf, svg_width, svg_height, |buf| {
571+
Svg::g(buf, "black", |buf| {
572+
Svg::line(
573+
buf,
574+
0,
575+
x_axis_location,
576+
svg_width as i32,
577+
x_axis_location,
578+
axis_weight,
579+
)?;
580+
Svg::line(
581+
buf,
582+
y_axis_location,
583+
0,
584+
y_axis_location,
585+
svg_height as i32,
586+
axis_weight,
587+
)?;
588+
589+
let mut seen = HashSet::new();
590+
draw_scaffold(buf, &mut seen, &self.left_wall)?;
591+
draw_scaffold(buf, &mut seen, &self.right_wall)
592+
})
593+
})
594+
.unwrap();
595+
buf
596+
}
458597
}
459598

460599
impl Display for Thermograph {

src/short/partizan/trajectory.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ impl Trajectory {
9595
self.x_intercepts[0]
9696
}
9797

98-
/// Gets the value of this trajectory at the specified point.
98+
/// Gets the value of this trajectory at the specified height (y value).
9999
pub fn value_at(&self, r: Rational) -> Rational {
100100
let i = self
101101
.critical_points

0 commit comments

Comments
 (0)