1use crate::binary::be_u32_at;
16use crate::vector::{VectorData, VectorObject};
17
18const LINE_HEADER: [u8; 92] = hex(concat!(
21 "0000005c0000004c00000058000000580000000000000011000000000000000000000000000000000000000000000000",
22 "00000000a244a2445f1f5f1f54ec54ec3ff0000000000000000000000000000240040000000000000f806b70",
23));
24const LINE_POINT: [u8; 88] = hex(concat!(
25 "000000000000000000000000000000000000000000000000000000000000000000000001",
26 "3f8000003f80000000000000000000003f8000003f8000003f800000000000000000000000000000",
27 "000000000f806b7000000000",
28));
29const OPEN_FLAGS: u32 = 0x11;
30const CLOSED_FLAGS: u32 = 0x111;
32const HEADER_POINT_COUNT: usize = 16;
33const HEADER_FLAGS: usize = 20;
34const HEADER_BBOX: usize = 24;
35const HEADER_COLOR: usize = 40;
37const HEADER_BRUSH_STYLE: usize = 76;
40const BRUSH_SIZE_FROM_END: usize = 12;
41const POINT_BBOX: usize = 16;
42const POINT_HAS_SEGMENT: usize = 32;
44const POINT_WIDTH: usize = 36;
46const FRAME_POINT_MITER: usize = 64;
49const FRAME_HEADER_MIN: usize = 96;
50const FRAME_POINT_MIN: usize = 80;
51
52const fn hex<const N: usize>(text: &str) -> [u8; N] {
53 const fn digit(d: u8) -> u8 {
54 match d {
55 b'0'..=b'9' => d - b'0',
56 b'a'..=b'f' => d - b'a' + 10,
57 _ => panic!("not a hex digit"),
58 }
59 }
60 let bytes = text.as_bytes();
61 assert!(bytes.len() == N * 2, "hex length does not match");
62 let mut result = [0u8; N];
63 let mut i = 0;
64 while i < N {
65 result[i] = (digit(bytes[2 * i]) << 4) | digit(bytes[2 * i + 1]);
66 i += 1;
67 }
68 result
69}
70
71fn put(data: &mut [u8], offset: usize, bytes: &[u8]) {
72 data[offset..offset + bytes.len()].copy_from_slice(bytes);
73}
74
75fn put_bbox(data: &mut [u8], offset: usize, bbox: [i32; 4]) {
76 for (i, value) in bbox.iter().enumerate() {
77 put(data, offset + 4 * i, &value.to_be_bytes());
78 }
79}
80
81fn segment_box(a: (f64, f64), b: (f64, f64), radius: f64) -> [i32; 4] {
83 [
84 (a.0.min(b.0) - radius).floor() as i32,
85 (a.1.min(b.1) - radius).floor() as i32,
86 (a.0.max(b.0) + radius).ceil() as i32,
87 (a.1.max(b.1) + radius).ceil() as i32,
88 ]
89}
90
91fn points_box(points: &[(f64, f64)], radius: f64) -> [i32; 4] {
92 points.iter().fold(segment_box(points[0], points[0], radius), |acc, &point| {
93 let one = segment_box(point, point, radius);
94 [acc[0].min(one[0]), acc[1].min(one[1]), acc[2].max(one[2]), acc[3].max(one[3])]
95 })
96}
97
98fn fill_header(header: &mut [u8], count: usize, bbox: [i32; 4], brush_style: u32, radius: f64) {
101 put(header, HEADER_POINT_COUNT, &(count as u32).to_be_bytes());
102 put_bbox(header, HEADER_BBOX, bbox);
103 put(header, HEADER_BRUSH_STYLE, &brush_style.to_be_bytes());
104 let size_offset = header.len() - BRUSH_SIZE_FROM_END;
105 put(header, size_offset, &radius.to_be_bytes());
106}
107
108#[derive(Debug, Clone, PartialEq)]
110pub struct NewVectorLine {
111 pub points: Vec<(f64, f64, f64)>,
113 pub closed: bool,
115 pub width_px: f64,
117}
118
119impl NewVectorLine {
120 pub fn xy(&self) -> Vec<(f64, f64)> {
122 self.points.iter().map(|&(x, y, _)| (x, y)).collect()
123 }
124}
125
126#[derive(Debug, Clone, PartialEq)]
129pub struct NewFrame {
130 pub points: Vec<(f64, f64)>,
132 pub width_px: f64,
134}
135
136#[derive(Debug, Clone, PartialEq)]
140pub struct FrameTemplate {
141 header: Vec<u8>,
142 point: Vec<u8>,
143}
144
145impl FrameTemplate {
146 pub fn of(data: &VectorData) -> Option<Self> {
149 let object = data.objects.first()?;
150 if object.header.len() < FRAME_HEADER_MIN || object.point_size < FRAME_POINT_MIN {
151 return None;
152 }
153 Some(FrameTemplate { header: object.header.clone(), point: object.points.first()?.clone() })
154 }
155
156 pub fn brush_style(&self) -> u32 {
158 be_u32_at(&self.header, HEADER_BRUSH_STYLE)
159 }
160}
161
162impl VectorObject {
163 pub fn brush_style(&self) -> Option<u32> {
167 match self.header.len() {
168 76 | 88 => Some(be_u32_at(&self.header, 72)),
169 size if size >= 92 => Some(be_u32_at(&self.header, HEADER_BRUSH_STYLE)),
170 _ => None,
171 }
172 }
173
174 pub fn new_line(line: &NewVectorLine, brush_style: u32) -> Option<VectorObject> {
177 if line.points.len() < 2 {
178 return None;
179 }
180 let radius = (line.width_px / 2.0).max(0.5);
181 let points = line.xy();
182 let mut header = LINE_HEADER.to_vec();
183 fill_header(&mut header, points.len(), points_box(&points, radius), brush_style, radius);
184 put(&mut header, HEADER_FLAGS, &(if line.closed { CLOSED_FLAGS } else { OPEN_FLAGS }).to_be_bytes());
185 header[HEADER_COLOR..HEADER_COLOR + 12].fill(0);
186 let raw_points = line
187 .points
188 .iter()
189 .enumerate()
190 .map(|(index, &(x, y, width))| {
191 let next = match points.get(index + 1) {
192 Some(&next) => Some(next),
193 None if line.closed => Some(points[0]),
194 None => None,
195 };
196 let mut point = LINE_POINT.to_vec();
197 put(&mut point, 0, &x.to_be_bytes());
198 put(&mut point, 8, &y.to_be_bytes());
199 put_bbox(&mut point, POINT_BBOX, segment_box((x, y), next.unwrap_or((x, y)), radius));
200 put(&mut point, POINT_HAS_SEGMENT, &(next.is_some() as u32).to_be_bytes());
201 put(&mut point, POINT_WIDTH, &(width.clamp(0.0, 1.0) as f32).to_be_bytes());
202 point
203 })
204 .collect();
205 Some(VectorObject { header, point_size: LINE_POINT.len(), points: raw_points })
206 }
207
208 pub fn new_frame(frame: &NewFrame, template: &FrameTemplate, brush_style: u32) -> Option<VectorObject> {
211 let points = &frame.points;
212 if points.len() < 3 {
213 return None;
214 }
215 let radius = (frame.width_px / 2.0).max(0.5);
216 let reach = radius + 2.0;
217 let mut header = template.header.clone();
218 fill_header(&mut header, points.len(), points_box(points, reach), brush_style, radius);
219 let raw_points = points
220 .iter()
221 .zip(frame_corners(points, radius))
222 .enumerate()
223 .map(|(index, (&(x, y), (bisector, miter)))| {
224 let next = points[(index + 1) % points.len()];
225 let mut point = template.point.clone();
226 put(&mut point, 0, &x.to_be_bytes());
227 put(&mut point, 8, &y.to_be_bytes());
228 put_bbox(&mut point, POINT_BBOX, segment_box((x, y), next, reach));
229 for (offset, value) in [(0, miter), (4, bisector.0), (8, bisector.1)] {
230 put(&mut point, FRAME_POINT_MITER + offset, &(value as f32).to_be_bytes());
231 }
232 point
233 })
234 .collect();
235 Some(VectorObject { header, point_size: template.point.len(), points: raw_points })
236 }
237}
238
239fn frame_corners(points: &[(f64, f64)], radius: f64) -> Vec<((f64, f64), f64)> {
241 let n = points.len();
242 let unit = |(x, y): (f64, f64)| {
243 let length = x.hypot(y);
244 if length > 1e-9 { (x / length, y / length) } else { (0.0, 0.0) }
245 };
246 let area: f64 = (0..n)
248 .map(|i| {
249 let (a, b) = (points[i], points[(i + 1) % n]);
250 a.0 * b.1 - b.0 * a.1
251 })
252 .sum();
253 let side = if area >= 0.0 { 1.0 } else { -1.0 };
254 (0..n)
255 .map(|i| {
256 let (a, b, c) = (points[(i + n - 1) % n], points[i], points[(i + 1) % n]);
257 let incoming = unit((b.0 - a.0, b.1 - a.1));
258 let outgoing = unit((c.0 - b.0, c.1 - b.1));
259 let normal = |(x, y): (f64, f64)| (y * side, -x * side);
261 let (n1, n2) = (normal(incoming), normal(outgoing));
262 let bisector = unit((n1.0 + n2.0, n1.1 + n2.1));
263 let bisector = if bisector == (0.0, 0.0) { n2 } else { bisector };
264 let cos = (bisector.0 * n2.0 + bisector.1 * n2.1).max(0.1);
266 (bisector, radius / cos)
267 })
268 .collect()
269}
270
271impl VectorData {
272 pub fn from_lines(lines: &[NewVectorLine], brush_style: u32) -> VectorData {
274 let objects = lines.iter().filter_map(|line| VectorObject::new_line(line, brush_style)).collect();
275 VectorData { lead: 0, objects, trailing: Vec::new() }
276 }
277
278 pub fn from_frames(frames: &[NewFrame], template: &FrameTemplate, brush_style: u32) -> VectorData {
281 let objects = frames.iter().filter_map(|frame| VectorObject::new_frame(frame, template, brush_style)).collect();
282 VectorData { lead: 0, objects, trailing: Vec::new() }
283 }
284
285 pub fn brush_style(&self) -> Option<u32> {
287 self.objects.first()?.brush_style()
288 }
289}
290
291pub fn shape_signature<'a>(lines: impl IntoIterator<Item = &'a [(f64, f64)]>) -> String {
294 let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
296 let mut feed = |value: i64| {
297 for byte in value.to_le_bytes() {
298 hash ^= u64::from(byte);
299 hash = hash.wrapping_mul(0x0100_0000_01b3);
300 }
301 };
302 let mut count = 0;
303 for points in lines {
304 count += 1;
305 feed(points.len() as i64);
306 for (x, y) in points {
307 feed(x.round() as i64);
308 feed(y.round() as i64);
309 }
310 }
311 format!("{count}:{hash:016x}")
312}
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317
318 fn line(points: &[(f64, f64)], closed: bool) -> NewVectorLine {
319 NewVectorLine { points: points.iter().map(|&(x, y)| (x, y, 1.0)).collect(), closed, width_px: 6.0 }
320 }
321
322 #[test]
323 fn lines_parse_back() {
324 let lines = [line(&[(10.0, 20.0), (30.5, 40.0), (50.0, 20.0)], false), line(&[(1.0, 1.0)], false)];
325 let bytes = VectorData::from_lines(&lines, 7).to_bytes();
326 let data = VectorData::parse(&bytes).unwrap();
327 assert_eq!(data.objects.len(), 1, "a one-point line is skipped");
328 let object = &data.objects[0];
329 assert_eq!(object.coordinates(), lines[0].xy());
330 assert_eq!(object.brush_size(), Some(3.0));
331 assert_eq!(object.brush_style(), Some(7));
332 assert_eq!(data.brush_style(), Some(7));
333 assert_eq!(object.bbox(), [7, 17, 53, 43]);
334 assert_eq!(object.main_color(), [0, 0, 0]);
335 assert!(!object.is_closed());
336 assert_eq!(object.point(1).unwrap().flags, 1);
338 assert_eq!(object.point(2).unwrap().flags, 0);
339 assert_eq!(object.point(2).unwrap().bbox, [47, 17, 53, 23]);
340 assert_eq!(object.point(0).unwrap().values[0], 1.0);
341 }
342
343 #[test]
344 fn closed_lines_join_the_ends() {
345 let bytes = VectorData::from_lines(&[line(&[(0.0, 0.0), (10.0, 0.0), (10.0, 10.0)], true)], 2).to_bytes();
346 let object = &VectorData::parse(&bytes).unwrap().objects[0];
347 assert_eq!(object.flags(), CLOSED_FLAGS);
348 let last = object.point(2).unwrap();
349 assert_eq!(last.flags, 1);
350 assert_eq!(last.bbox, [-3, -3, 13, 13]);
351 }
352
353 #[test]
354 fn frames_copy_the_template_and_set_corners() {
355 let mut header = vec![0u8; 96];
356 put(&mut header, 0, &96u32.to_be_bytes());
357 put(&mut header, 8, &88u32.to_be_bytes());
358 put(&mut header, 20, &0x1111u32.to_be_bytes());
359 put(&mut header, 92, &[0xab; 4]);
360 let template_data = VectorData {
361 lead: 0,
362 objects: vec![VectorObject { header, point_size: 88, points: vec![vec![0x5a; 88]] }],
363 trailing: vec![],
364 };
365 let template = FrameTemplate::of(&template_data).unwrap();
366 let square = NewFrame { points: vec![(0.0, 0.0), (100.0, 0.0), (100.0, 50.0), (0.0, 50.0)], width_px: 4.0 };
367 let data = VectorData::from_frames(std::slice::from_ref(&square), &template, 9);
368 let parsed = VectorData::parse(&data.to_bytes()).unwrap();
369 let object = &parsed.objects[0];
370 assert_eq!(object.coordinates(), square.points);
371 assert_eq!(object.flags(), 0x1111, "flags come from the template");
372 assert_eq!(&object.header[92..96], &[0xab; 4], "undecoded bytes are kept");
373 assert_eq!(object.brush_size(), Some(2.0));
374 assert_eq!(object.brush_style(), Some(9));
375 assert_eq!(object.bbox(), [-4, -4, 104, 54]);
376 let point = object.point(0).unwrap();
378 let miter = f32::from_bits(be_u32_at(&object.points[0], FRAME_POINT_MITER));
379 assert!((miter - 2.0 * std::f32::consts::SQRT_2).abs() < 1e-4);
380 let bisector = (point.values[8], point.values[9]);
381 let diagonal = std::f32::consts::FRAC_1_SQRT_2;
382 assert!((bisector.0 + diagonal).abs() < 1e-4 && (bisector.1 + diagonal).abs() < 1e-4, "{bisector:?}");
383 assert_eq!(&object.points[0][76..88], &[0x5a; 12]);
384 }
385
386 #[test]
387 fn signatures_follow_rounded_points() {
388 let a = [(1.2, 2.0), (3.0, 4.0)];
389 let b = [(0.9, 2.4), (3.0, 4.0)];
390 assert_eq!(shape_signature([&a[..]]), shape_signature([&b[..]]));
391 assert_ne!(shape_signature([&a[..]]), shape_signature([&a[..1]]));
392 }
393}