jagua_rs/io/svg/
svg_util.rs

1use crate::collision_detection::hazards::filter::HazardFilter;
2use crate::collision_detection::quadtree::{QTHazPresence, QTNode};
3use crate::entities::N_QUALITIES;
4use crate::geometry;
5use crate::geometry::OriginalShape;
6use crate::geometry::primitives::{Edge, Point, SPolygon};
7use serde::{Deserialize, Deserializer, Serialize, Serializer};
8use std::fmt::{Display, Formatter};
9use svg::node::element::path::Data;
10use svg::node::element::{Circle, Path};
11
12#[allow(clippy::struct_excessive_bools)]
13#[derive(Clone, PartialEq, Debug, Serialize, Deserialize, Copy)]
14#[serde(default)]
15pub struct SvgDrawOptions {
16    ///The theme to use for the svg
17    pub theme: SvgLayoutTheme,
18    ///Draw the quadtree on top
19    pub quadtree: bool,
20    ///Draw the fail fast surrogate on top of each item
21    pub surrogate: bool,
22    ///Draw dashed lines between colliding items
23    pub highlight_collisions: bool,
24    ///Draw the modified shapes used internally instead of the original ones
25    pub draw_cd_shapes: bool,
26    ///Highlights the shapes used for collision detection with a dashed border
27    pub highlight_cd_shapes: bool,
28}
29
30impl Default for SvgDrawOptions {
31    fn default() -> Self {
32        Self {
33            theme: SvgLayoutTheme::default(),
34            quadtree: false,
35            surrogate: false,
36            highlight_collisions: true,
37            draw_cd_shapes: false,
38            highlight_cd_shapes: true,
39        }
40    }
41}
42
43#[derive(Clone, PartialEq, Debug, Serialize, Deserialize, Copy)]
44pub struct SvgLayoutTheme {
45    pub stroke_width_multiplier: f32,
46    pub container_fill: Color,
47    pub item_fill: Color,
48    pub hole_fill: Color,
49    pub qz_fill: [Color; N_QUALITIES],
50    pub qz_stroke_opac: f32,
51    pub collision_highlight_color: Color,
52}
53
54impl Default for SvgLayoutTheme {
55    fn default() -> Self {
56        SvgLayoutTheme::EARTH_TONES
57    }
58}
59
60impl SvgLayoutTheme {
61    pub const EARTH_TONES: SvgLayoutTheme = SvgLayoutTheme {
62        stroke_width_multiplier: 2.0,
63        container_fill: Color(0xCC, 0x82, 0x4A),
64        item_fill: Color(0xFF, 0xC8, 0x79),
65        hole_fill: Color(0x2D, 0x2D, 0x2D),
66        qz_fill: [
67            Color(0x00, 0x00, 0x00), // BLACK
68            Color(0xFF, 0x00, 0x00), // RED
69            Color(0xFF, 0x5E, 0x00), // ORANGE
70            Color(0xFF, 0xA5, 0x00), // LIGHT ORANGE
71            Color(0xC7, 0xA9, 0x00), // DARK YELLOW
72            Color(0xFF, 0xFF, 0x00), // YELLOW
73            Color(0xCB, 0xFF, 0x00), // GREEN
74            Color(0xCB, 0xFF, 0x00), // GREEN
75            Color(0xCB, 0xFF, 0x00), // GREEN
76            Color(0xCB, 0xFF, 0x00), // GREEN
77        ],
78        qz_stroke_opac: 0.5,
79        collision_highlight_color: Color(0x00, 0xFF, 0x00), // LIME
80    };
81
82    pub const GRAY: SvgLayoutTheme = SvgLayoutTheme {
83        stroke_width_multiplier: 2.5,
84        container_fill: Color(0xD3, 0xD3, 0xD3),
85        item_fill: Color(0x7A, 0x7A, 0x7A),
86        hole_fill: Color(0xFF, 0xFF, 0xFF),
87        qz_fill: [
88            Color(0x63, 0x63, 0x63), //GRAY
89            Color(0x63, 0x63, 0x63), //GRAY
90            Color(0x63, 0x63, 0x63), //GRAY
91            Color(0x63, 0x63, 0x63), //GRAY
92            Color(0x63, 0x63, 0x63), //GRAY
93            Color(0x63, 0x63, 0x63), //GRAY
94            Color(0x63, 0x63, 0x63), //GRAY
95            Color(0x63, 0x63, 0x63), //GRAY
96            Color(0x63, 0x63, 0x63), //GRAY
97            Color(0x63, 0x63, 0x63), //GRAY
98        ],
99        qz_stroke_opac: 0.9,
100        collision_highlight_color: Color(0xD0, 0x00, 0x00), //LIME
101    };
102}
103
104#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
105pub fn change_brightness(color: Color, fraction: f32) -> Color {
106    let Color(r, g, b) = color;
107
108    let r = (f32::from(r) * fraction) as u8;
109    let g = (f32::from(g) * fraction) as u8;
110    let b = (f32::from(b) * fraction) as u8;
111    Color(r, g, b)
112}
113
114#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
115pub fn blend_colors(color_1: Color, color_2: Color) -> Color {
116    //blend color_1 and color_2
117    let Color(r_1, g_1, b_1) = color_1;
118    let Color(r_2, g_2, b_2) = color_2;
119
120    let r = ((f32::from(r_1) * 0.5) + (f32::from(r_2) * 0.5)) as u8;
121    let g = ((f32::from(g_1) * 0.5) + (f32::from(g_2) * 0.5)) as u8;
122    let b = ((f32::from(b_1) * 0.5) + (f32::from(b_2) * 0.5)) as u8;
123
124    Color(r, g, b)
125}
126
127#[derive(Copy, Clone, PartialEq, Debug)]
128pub struct Color(u8, u8, u8);
129
130impl Display for Color {
131    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
132        write!(f, "#{:02X}{:02X}{:02X}", self.0, self.1, self.2)
133    }
134}
135
136impl From<String> for Color {
137    fn from(mut s: String) -> Self {
138        if s.starts_with('#') {
139            s.remove(0);
140        }
141        let r = u8::from_str_radix(&s[0..2], 16).unwrap();
142        let g = u8::from_str_radix(&s[2..4], 16).unwrap();
143        let b = u8::from_str_radix(&s[4..6], 16).unwrap();
144        Color(r, g, b)
145    }
146}
147
148impl From<&str> for Color {
149    fn from(s: &str) -> Self {
150        Color::from(s.to_owned())
151    }
152}
153
154impl Serialize for Color {
155    fn serialize<S>(&self, serializer: S) -> Result<<S as Serializer>::Ok, <S as Serializer>::Error>
156    where
157        S: Serializer,
158    {
159        serializer.serialize_str(&format!("{self}"))
160    }
161}
162
163impl<'de> Deserialize<'de> for Color {
164    fn deserialize<D>(deserializer: D) -> Result<Self, <D as Deserializer<'de>>::Error>
165    where
166        D: Deserializer<'de>,
167    {
168        let s = String::deserialize(deserializer)?;
169        Ok(Color::from(s))
170    }
171}
172
173pub fn original_shape_data(
174    original: &OriginalShape,
175    internal: &SPolygon,
176    draw_internal: bool,
177) -> Data {
178    if draw_internal {
179        simple_polygon_data(internal)
180    } else {
181        simple_polygon_data(&original.shape)
182    }
183}
184
185pub fn simple_polygon_data(s_poly: &SPolygon) -> Data {
186    let mut data = Data::new().move_to::<(f32, f32)>(s_poly.vertex(0).into());
187    for i in 1..s_poly.n_vertices() {
188        data = data.line_to::<(f32, f32)>(s_poly.vertex(i).into());
189    }
190    data.close()
191}
192
193pub fn quad_tree_data(
194    qt_root: &QTNode,
195    irrelevant_hazards: &impl HazardFilter,
196) -> (Data, Data, Data) {
197    qt_node_data(
198        qt_root,
199        Data::new(),
200        Data::new(),
201        Data::new(),
202        irrelevant_hazards,
203    )
204}
205
206#[allow(clippy::similar_names)]
207fn qt_node_data(
208    qt_node: &QTNode,
209    mut data_eh: Data, //entire hazards data
210    mut data_ph: Data, //partial hazards data
211    mut data_nh: Data, //no hazards data
212    filter: &impl HazardFilter,
213) -> (Data, Data, Data) {
214    //Only draw qt_nodes that do not have a child
215
216    if let (Some(children), Some(_)) =
217        (qt_node.children.as_ref(), qt_node.hazards.strongest(filter))
218    {
219        //not a leaf node, go to children
220        for child in children.iter() {
221            let data = qt_node_data(child, data_eh, data_ph, data_nh, filter);
222            data_eh = data.0;
223            data_ph = data.1;
224            data_nh = data.2;
225        }
226    } else {
227        //leaf node, draw it
228        let rect = &qt_node.bbox;
229        let draw = |data: Data| -> Data {
230            data.move_to((rect.x_min, rect.y_min))
231                .line_to((rect.x_max, rect.y_min))
232                .line_to((rect.x_max, rect.y_max))
233                .line_to((rect.x_min, rect.y_max))
234                .close()
235        };
236
237        match qt_node.hazards.strongest(filter) {
238            Some(ch) => match ch.presence {
239                QTHazPresence::Entire => data_eh = draw(data_eh),
240                QTHazPresence::Partial(_) => data_ph = draw(data_ph),
241                QTHazPresence::None => unreachable!(),
242            },
243            None => data_nh = draw(data_nh),
244        }
245    }
246
247    (data_eh, data_ph, data_nh)
248}
249
250pub fn data_to_path(data: Data, params: &[(&str, &str)]) -> Path {
251    let mut path = Path::new();
252    for param in params {
253        path = path.set(param.0, param.1);
254    }
255    path.set("d", data)
256}
257
258pub fn point(Point(x, y): Point, fill: Option<&str>, rad: Option<f32>) -> Circle {
259    Circle::new()
260        .set("cx", x)
261        .set("cy", y)
262        .set("r", rad.unwrap_or(0.5))
263        .set("fill", fill.unwrap_or("black"))
264}
265
266pub fn circle(circle: geometry::primitives::Circle, params: &[(&str, &str)]) -> Circle {
267    let mut circle = Circle::new()
268        .set("cx", circle.center.0)
269        .set("cy", circle.center.1)
270        .set("r", circle.radius);
271    for param in params {
272        circle = circle.set(param.0, param.1);
273    }
274    circle
275}
276
277pub fn edge_data(edge: Edge) -> Data {
278    Data::new()
279        .move_to((edge.start.0, edge.start.1))
280        .line_to((edge.end.0, edge.end.1))
281}
282
283#[allow(dead_code)]
284pub fn aa_rect_data(rect: geometry::primitives::Rect) -> Data {
285    Data::new()
286        .move_to((rect.x_min, rect.y_min))
287        .line_to((rect.x_max, rect.y_min))
288        .line_to((rect.x_max, rect.y_max))
289        .line_to((rect.x_min, rect.y_max))
290        .close()
291}