tetratto_core/model/
carp.rs1use serde::{Serialize, Deserialize};
2
3pub const END_OF_HEADER: u8 = 0x1a;
16pub const COLOR: u8 = 0x1b;
20pub const SIZE: u8 = 0x2b;
22pub const LINE: u8 = 0x3b;
24pub const POINT: u8 = 0x4b;
31pub const EOF: u8 = 0x1f;
33
34#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
36#[repr(u8)]
37pub enum CommandType {
38 EndOfHeader = END_OF_HEADER,
40 Color = COLOR,
42 Size = SIZE,
44 Line = LINE,
46 Point = POINT,
48 Eof = EOF,
50}
51
52#[derive(Serialize, Deserialize, Debug, Clone)]
53pub struct Command {
54 pub r#type: CommandType,
56 pub data: Vec<u8>,
58}
59
60impl From<Command> for Vec<u8> {
61 fn from(val: Command) -> Self {
62 let mut d = val.data;
63 d.insert(0, val.r#type as u8);
64 d
65 }
66}
67
68#[derive(Serialize, Deserialize, Debug)]
72pub struct CarpGraph {
73 pub header: Vec<u8>,
74 pub dimensions: (u32, u32),
75 pub commands: Vec<Command>,
76}
77
78macro_rules! select_bytes {
79 ($count:literal, $from:ident) => {{
80 let mut data: Vec<u8> = Vec::new();
81 let mut seen_bytes = 0;
82
83 while let Some((_, byte)) = $from.next() {
84 seen_bytes += 1;
85 data.push(byte.to_owned());
86
87 if seen_bytes == $count {
88 break;
90 }
91 }
92
93 data
94 }};
95}
96
97macro_rules! spread {
98 ($into:ident, $from:expr) => {
99 for byte in &$from {
100 $into.push(byte.to_owned())
101 }
102 };
103}
104
105impl CarpGraph {
106 pub fn to_bytes(&self) -> Vec<u8> {
107 let mut out: Vec<u8> = Vec::new();
108
109 spread!(out, self.header);
111 spread!(out, self.dimensions.0.to_be_bytes()); spread!(out, self.dimensions.1.to_be_bytes()); out.push(END_OF_HEADER);
114
115 for command in &self.commands {
117 out.push(command.r#type as u8);
118 spread!(out, command.data);
119 }
120
121 out.push(EOF);
123 out
124 }
125
126 pub fn from_bytes(bytes: Vec<u8>) -> Self {
127 let mut header: Vec<u8> = Vec::new();
128 let mut dimensions: (u32, u32) = (0, 0);
129 let mut commands: Vec<Command> = Vec::new();
130
131 let mut in_header: bool = true;
132 let mut byte_buffer: Vec<u8> = Vec::new(); let mut bytes_iter = bytes.iter().enumerate();
135 while let Some((i, byte)) = bytes_iter.next() {
136 let byte = byte.to_owned();
137 match byte {
138 END_OF_HEADER => in_header = false,
139 COLOR => {
140 let data = select_bytes!(6, bytes_iter);
141 commands.push(Command {
142 r#type: CommandType::Color,
143 data,
144 });
145 }
146 SIZE => {
147 let data = select_bytes!(2, bytes_iter);
148 commands.push(Command {
149 r#type: CommandType::Size,
150 data,
151 });
152 }
153 POINT => {
154 let data = select_bytes!(8, bytes_iter);
155 commands.push(Command {
156 r#type: CommandType::Point,
157 data,
158 });
159 }
160 LINE => commands.push(Command {
161 r#type: CommandType::Line,
162 data: Vec::new(),
163 }),
164 EOF => break,
165 _ => {
166 if in_header {
167 if (0..2).contains(&i) {
168 header.push(byte);
170 } else if (2..4).contains(&i) {
171 header.push(byte);
173 } else if (4..8).contains(&i) {
174 byte_buffer.push(byte);
176
177 if i == 7 {
178 let (bytes, _) = byte_buffer.split_at(size_of::<u32>());
180 dimensions.0 = u32::from_be_bytes(bytes.try_into().unwrap());
181 byte_buffer = Vec::new();
182 }
183 } else if (8..12).contains(&i) {
184 byte_buffer.push(byte);
186
187 if i == 11 {
188 let (bytes, _) = byte_buffer.split_at(size_of::<u32>());
190 dimensions.1 = u32::from_be_bytes(bytes.try_into().unwrap());
191 byte_buffer = Vec::new();
192 }
193 }
194 } else {
195 println!("extraneous byte at {i}");
197 }
198 }
199 }
200 }
201
202 Self {
203 header,
204 dimensions,
205 commands,
206 }
207 }
208
209 pub fn to_svg(&self) -> String {
210 let mut out: String = String::new();
211 out.push_str(&format!(
212 "<svg viewBox=\"0 0 {} {}\" xmlns=\"http://www.w3.org/2000/svg\" width=\"{}\" height=\"{}\" style=\"background: white; width: {}px; height: {}px\" class=\"carpgraph\">",
213 self.dimensions.0, self.dimensions.1, self.dimensions.0, self.dimensions.1, self.dimensions.0, self.dimensions.1
214 ));
215
216 let mut stroke_size: u16 = 2;
218 let mut stroke_color: String = "000000".to_string();
219
220 let mut previous_x_y: Option<(u32, u32)> = None;
221 let mut line_path = String::new();
222
223 for command in &self.commands {
224 match command.r#type {
225 CommandType::Size => {
226 let (bytes, _) = command.data.split_at(size_of::<u16>());
227 stroke_size = u16::from_be_bytes(bytes.try_into().unwrap_or([0, 0]));
228 }
229 CommandType::Color => {
230 stroke_color =
231 String::from_utf8(command.data.to_owned()).unwrap_or("#000000".to_string())
232 }
233 CommandType::Line => {
234 if !line_path.is_empty() {
235 out.push_str(&format!(
236 "<path d=\"{line_path}\" stroke=\"#{stroke_color}\" stroke-width=\"{stroke_size}\" />"
237 ));
238 }
239
240 previous_x_y = None;
241 line_path = String::new();
242 }
243 CommandType::Point => {
244 let (x, y) = command.data.split_at(size_of::<u32>());
245 let point = ({ u32::from_be_bytes(x.try_into().unwrap()) }, {
246 u32::from_be_bytes(y.try_into().unwrap())
247 });
248
249 line_path.push_str(&format!(
251 " M{} {}{}",
252 point.0,
253 point.1,
254 if let Some(pxy) = previous_x_y {
255 format!(" L{} {}", pxy.0, pxy.1)
257 } else {
258 String::new()
259 }
260 ));
261
262 previous_x_y = Some((point.0, point.1));
263
264 out.push_str(&format!(
266 "<circle cx=\"{}\" cy=\"{}\" r=\"{}\" fill=\"#{stroke_color}\" />",
267 point.0,
268 point.1,
269 stroke_size / 2 ));
271 }
272 _ => unreachable!("never pushed to commands"),
273 }
274 }
275
276 if !line_path.is_empty() {
277 out.push_str(&format!(
278 "<path d=\"{line_path}\" stroke=\"#{stroke_color}\" stroke-width=\"{stroke_size}\" />"
279 ));
280 }
281
282 format!("{out}</svg>")
284 }
285}