use crate::binary::be_u32_at;
use crate::vector::{VectorData, VectorObject};
const LINE_HEADER: [u8; 92] = hex(concat!(
"0000005c0000004c00000058000000580000000000000011000000000000000000000000000000000000000000000000",
"00000000a244a2445f1f5f1f54ec54ec3ff0000000000000000000000000000240040000000000000f806b70",
));
const LINE_POINT: [u8; 88] = hex(concat!(
"000000000000000000000000000000000000000000000000000000000000000000000001",
"3f8000003f80000000000000000000003f8000003f8000003f800000000000000000000000000000",
"000000000f806b7000000000",
));
const OPEN_FLAGS: u32 = 0x11;
const CLOSED_FLAGS: u32 = 0x111;
const HEADER_POINT_COUNT: usize = 16;
const HEADER_FLAGS: usize = 20;
const HEADER_BBOX: usize = 24;
const HEADER_COLOR: usize = 40;
const HEADER_BRUSH_STYLE: usize = 76;
const BRUSH_SIZE_FROM_END: usize = 12;
const POINT_BBOX: usize = 16;
const POINT_HAS_SEGMENT: usize = 32;
const POINT_WIDTH: usize = 36;
const FRAME_POINT_MITER: usize = 64;
const FRAME_HEADER_MIN: usize = 96;
const FRAME_POINT_MIN: usize = 80;
const fn hex<const N: usize>(text: &str) -> [u8; N] {
const fn digit(d: u8) -> u8 {
match d {
b'0'..=b'9' => d - b'0',
b'a'..=b'f' => d - b'a' + 10,
_ => panic!("not a hex digit"),
}
}
let bytes = text.as_bytes();
assert!(bytes.len() == N * 2, "hex length does not match");
let mut result = [0u8; N];
let mut i = 0;
while i < N {
result[i] = (digit(bytes[2 * i]) << 4) | digit(bytes[2 * i + 1]);
i += 1;
}
result
}
fn put(data: &mut [u8], offset: usize, bytes: &[u8]) {
data[offset..offset + bytes.len()].copy_from_slice(bytes);
}
fn put_bbox(data: &mut [u8], offset: usize, bbox: [i32; 4]) {
for (i, value) in bbox.iter().enumerate() {
put(data, offset + 4 * i, &value.to_be_bytes());
}
}
fn segment_box(a: (f64, f64), b: (f64, f64), radius: f64) -> [i32; 4] {
[
(a.0.min(b.0) - radius).floor() as i32,
(a.1.min(b.1) - radius).floor() as i32,
(a.0.max(b.0) + radius).ceil() as i32,
(a.1.max(b.1) + radius).ceil() as i32,
]
}
fn points_box(points: &[(f64, f64)], radius: f64) -> [i32; 4] {
points.iter().fold(segment_box(points[0], points[0], radius), |acc, &point| {
let one = segment_box(point, point, radius);
[acc[0].min(one[0]), acc[1].min(one[1]), acc[2].max(one[2]), acc[3].max(one[3])]
})
}
fn fill_header(header: &mut [u8], count: usize, bbox: [i32; 4], brush_style: u32, radius: f64) {
put(header, HEADER_POINT_COUNT, &(count as u32).to_be_bytes());
put_bbox(header, HEADER_BBOX, bbox);
put(header, HEADER_BRUSH_STYLE, &brush_style.to_be_bytes());
let size_offset = header.len() - BRUSH_SIZE_FROM_END;
put(header, size_offset, &radius.to_be_bytes());
}
#[derive(Debug, Clone, PartialEq)]
pub struct NewVectorLine {
pub points: Vec<(f64, f64, f64)>,
pub closed: bool,
pub width_px: f64,
}
impl NewVectorLine {
pub fn xy(&self) -> Vec<(f64, f64)> {
self.points.iter().map(|&(x, y, _)| (x, y)).collect()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct NewFrame {
pub points: Vec<(f64, f64)>,
pub width_px: f64,
}
#[derive(Debug, Clone, PartialEq)]
pub struct FrameTemplate {
header: Vec<u8>,
point: Vec<u8>,
}
impl FrameTemplate {
pub fn of(data: &VectorData) -> Option<Self> {
let object = data.objects.first()?;
if object.header.len() < FRAME_HEADER_MIN || object.point_size < FRAME_POINT_MIN {
return None;
}
Some(FrameTemplate { header: object.header.clone(), point: object.points.first()?.clone() })
}
pub fn brush_style(&self) -> u32 {
be_u32_at(&self.header, HEADER_BRUSH_STYLE)
}
}
impl VectorObject {
pub fn brush_style(&self) -> Option<u32> {
match self.header.len() {
76 | 88 => Some(be_u32_at(&self.header, 72)),
size if size >= 92 => Some(be_u32_at(&self.header, HEADER_BRUSH_STYLE)),
_ => None,
}
}
pub fn new_line(line: &NewVectorLine, brush_style: u32) -> Option<VectorObject> {
if line.points.len() < 2 {
return None;
}
let radius = (line.width_px / 2.0).max(0.5);
let points = line.xy();
let mut header = LINE_HEADER.to_vec();
fill_header(&mut header, points.len(), points_box(&points, radius), brush_style, radius);
put(&mut header, HEADER_FLAGS, &(if line.closed { CLOSED_FLAGS } else { OPEN_FLAGS }).to_be_bytes());
header[HEADER_COLOR..HEADER_COLOR + 12].fill(0);
let raw_points = line
.points
.iter()
.enumerate()
.map(|(index, &(x, y, width))| {
let next = match points.get(index + 1) {
Some(&next) => Some(next),
None if line.closed => Some(points[0]),
None => None,
};
let mut point = LINE_POINT.to_vec();
put(&mut point, 0, &x.to_be_bytes());
put(&mut point, 8, &y.to_be_bytes());
put_bbox(&mut point, POINT_BBOX, segment_box((x, y), next.unwrap_or((x, y)), radius));
put(&mut point, POINT_HAS_SEGMENT, &(next.is_some() as u32).to_be_bytes());
put(&mut point, POINT_WIDTH, &(width.clamp(0.0, 1.0) as f32).to_be_bytes());
point
})
.collect();
Some(VectorObject { header, point_size: LINE_POINT.len(), points: raw_points })
}
pub fn new_frame(frame: &NewFrame, template: &FrameTemplate, brush_style: u32) -> Option<VectorObject> {
let points = &frame.points;
if points.len() < 3 {
return None;
}
let radius = (frame.width_px / 2.0).max(0.5);
let reach = radius + 2.0;
let mut header = template.header.clone();
fill_header(&mut header, points.len(), points_box(points, reach), brush_style, radius);
let raw_points = points
.iter()
.zip(frame_corners(points, radius))
.enumerate()
.map(|(index, (&(x, y), (bisector, miter)))| {
let next = points[(index + 1) % points.len()];
let mut point = template.point.clone();
put(&mut point, 0, &x.to_be_bytes());
put(&mut point, 8, &y.to_be_bytes());
put_bbox(&mut point, POINT_BBOX, segment_box((x, y), next, reach));
for (offset, value) in [(0, miter), (4, bisector.0), (8, bisector.1)] {
put(&mut point, FRAME_POINT_MITER + offset, &(value as f32).to_be_bytes());
}
point
})
.collect();
Some(VectorObject { header, point_size: template.point.len(), points: raw_points })
}
}
fn frame_corners(points: &[(f64, f64)], radius: f64) -> Vec<((f64, f64), f64)> {
let n = points.len();
let unit = |(x, y): (f64, f64)| {
let length = x.hypot(y);
if length > 1e-9 { (x / length, y / length) } else { (0.0, 0.0) }
};
let area: f64 = (0..n)
.map(|i| {
let (a, b) = (points[i], points[(i + 1) % n]);
a.0 * b.1 - b.0 * a.1
})
.sum();
let side = if area >= 0.0 { 1.0 } else { -1.0 };
(0..n)
.map(|i| {
let (a, b, c) = (points[(i + n - 1) % n], points[i], points[(i + 1) % n]);
let incoming = unit((b.0 - a.0, b.1 - a.1));
let outgoing = unit((c.0 - b.0, c.1 - b.1));
let normal = |(x, y): (f64, f64)| (y * side, -x * side);
let (n1, n2) = (normal(incoming), normal(outgoing));
let bisector = unit((n1.0 + n2.0, n1.1 + n2.1));
let bisector = if bisector == (0.0, 0.0) { n2 } else { bisector };
let cos = (bisector.0 * n2.0 + bisector.1 * n2.1).max(0.1);
(bisector, radius / cos)
})
.collect()
}
impl VectorData {
pub fn from_lines(lines: &[NewVectorLine], brush_style: u32) -> VectorData {
let objects = lines.iter().filter_map(|line| VectorObject::new_line(line, brush_style)).collect();
VectorData { lead: 0, objects, trailing: Vec::new() }
}
pub fn from_frames(frames: &[NewFrame], template: &FrameTemplate, brush_style: u32) -> VectorData {
let objects = frames.iter().filter_map(|frame| VectorObject::new_frame(frame, template, brush_style)).collect();
VectorData { lead: 0, objects, trailing: Vec::new() }
}
pub fn brush_style(&self) -> Option<u32> {
self.objects.first()?.brush_style()
}
}
pub fn shape_signature<'a>(lines: impl IntoIterator<Item = &'a [(f64, f64)]>) -> String {
let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
let mut feed = |value: i64| {
for byte in value.to_le_bytes() {
hash ^= u64::from(byte);
hash = hash.wrapping_mul(0x0100_0000_01b3);
}
};
let mut count = 0;
for points in lines {
count += 1;
feed(points.len() as i64);
for (x, y) in points {
feed(x.round() as i64);
feed(y.round() as i64);
}
}
format!("{count}:{hash:016x}")
}
#[cfg(test)]
mod tests {
use super::*;
fn line(points: &[(f64, f64)], closed: bool) -> NewVectorLine {
NewVectorLine { points: points.iter().map(|&(x, y)| (x, y, 1.0)).collect(), closed, width_px: 6.0 }
}
#[test]
fn lines_parse_back() {
let lines = [line(&[(10.0, 20.0), (30.5, 40.0), (50.0, 20.0)], false), line(&[(1.0, 1.0)], false)];
let bytes = VectorData::from_lines(&lines, 7).to_bytes();
let data = VectorData::parse(&bytes).unwrap();
assert_eq!(data.objects.len(), 1, "a one-point line is skipped");
let object = &data.objects[0];
assert_eq!(object.coordinates(), lines[0].xy());
assert_eq!(object.brush_size(), Some(3.0));
assert_eq!(object.brush_style(), Some(7));
assert_eq!(data.brush_style(), Some(7));
assert_eq!(object.bbox(), [7, 17, 53, 43]);
assert_eq!(object.main_color(), [0, 0, 0]);
assert!(!object.is_closed());
assert_eq!(object.point(1).unwrap().flags, 1);
assert_eq!(object.point(2).unwrap().flags, 0);
assert_eq!(object.point(2).unwrap().bbox, [47, 17, 53, 23]);
assert_eq!(object.point(0).unwrap().values[0], 1.0);
}
#[test]
fn closed_lines_join_the_ends() {
let bytes = VectorData::from_lines(&[line(&[(0.0, 0.0), (10.0, 0.0), (10.0, 10.0)], true)], 2).to_bytes();
let object = &VectorData::parse(&bytes).unwrap().objects[0];
assert_eq!(object.flags(), CLOSED_FLAGS);
let last = object.point(2).unwrap();
assert_eq!(last.flags, 1);
assert_eq!(last.bbox, [-3, -3, 13, 13]);
}
#[test]
fn frames_copy_the_template_and_set_corners() {
let mut header = vec![0u8; 96];
put(&mut header, 0, &96u32.to_be_bytes());
put(&mut header, 8, &88u32.to_be_bytes());
put(&mut header, 20, &0x1111u32.to_be_bytes());
put(&mut header, 92, &[0xab; 4]);
let template_data = VectorData {
lead: 0,
objects: vec![VectorObject { header, point_size: 88, points: vec![vec![0x5a; 88]] }],
trailing: vec![],
};
let template = FrameTemplate::of(&template_data).unwrap();
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 };
let data = VectorData::from_frames(std::slice::from_ref(&square), &template, 9);
let parsed = VectorData::parse(&data.to_bytes()).unwrap();
let object = &parsed.objects[0];
assert_eq!(object.coordinates(), square.points);
assert_eq!(object.flags(), 0x1111, "flags come from the template");
assert_eq!(&object.header[92..96], &[0xab; 4], "undecoded bytes are kept");
assert_eq!(object.brush_size(), Some(2.0));
assert_eq!(object.brush_style(), Some(9));
assert_eq!(object.bbox(), [-4, -4, 104, 54]);
let point = object.point(0).unwrap();
let miter = f32::from_bits(be_u32_at(&object.points[0], FRAME_POINT_MITER));
assert!((miter - 2.0 * std::f32::consts::SQRT_2).abs() < 1e-4);
let bisector = (point.values[8], point.values[9]);
let diagonal = std::f32::consts::FRAC_1_SQRT_2;
assert!((bisector.0 + diagonal).abs() < 1e-4 && (bisector.1 + diagonal).abs() < 1e-4, "{bisector:?}");
assert_eq!(&object.points[0][76..88], &[0x5a; 12]);
}
#[test]
fn signatures_follow_rounded_points() {
let a = [(1.2, 2.0), (3.0, 4.0)];
let b = [(0.9, 2.4), (3.0, 4.0)];
assert_eq!(shape_signature([&a[..]]), shape_signature([&b[..]]));
assert_ne!(shape_signature([&a[..]]), shape_signature([&a[..1]]));
}
}