Skip to main content

csp_format/
vector_build.rs

1//! New vector objects: lines and コマ枠 (frame) borders (from name-studio's
2//! clip-core).
3//!
4//! A line is written in the layout CSP 5 gives a pen or polyline object
5//! (92-byte header, 88-byte points; see `docs/spec/05-vector.md`), with the
6//! fields that are not decoded copied from a CSP-made object. A frame
7//! border copies the header and first point of a border CSP made
8//! ([`FrameTemplate`]) and sets the corners' miter values, which CSP uses to
9//! draw the border's outside edge.
10//!
11//! Objects name their brush by `BrushStyle.MainId`, so the brush style id
12//! must exist in the file the data goes into: take it from an object of the
13//! same file ([`VectorObject::brush_style`]).
14
15use crate::binary::be_u32_at;
16use crate::vector::{VectorData, VectorObject};
17
18/// Header and point CSP 5 writes for a pen or polyline object; fields this
19/// module does not set keep these values.
20const 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;
30/// Closed polylines (CSP's 折れ線 with 線を閉じる) add 0x100.
31const CLOSED_FLAGS: u32 = 0x111;
32const HEADER_POINT_COUNT: usize = 16;
33const HEADER_FLAGS: usize = 20;
34const HEADER_BBOX: usize = 24;
35/// Main colour, `u32` × 3 (black is all zero).
36const HEADER_COLOR: usize = 40;
37/// The brush style id new objects are drawn with (the second id of a
38/// 92-byte header).
39const HEADER_BRUSH_STYLE: usize = 76;
40const BRUSH_SIZE_FROM_END: usize = 12;
41const POINT_BBOX: usize = 16;
42/// 1 when a segment leaves the point (all but the last point of an open line).
43const POINT_HAS_SEGMENT: usize = 32;
44/// Width factor of the point (pen pressure), `f32`.
45const POINT_WIDTH: usize = 36;
46/// Border corners keep the miter length (`f32`) and the outward unit
47/// bisector (`f32` × 2) after the point's common fields.
48const 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
81/// Box of the segment a → b widened by `radius`, in whole pixels.
82fn 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
98/// Set the point count, bounding box, brush style and brush size (radius)
99/// of a copied header.
100fn 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/// A line to draw as a new vector object, in layer pixels.
109#[derive(Debug, Clone, PartialEq)]
110pub struct NewVectorLine {
111    /// `(x, y, width factor)`; the factor (pen pressure) is 0..1.
112    pub points: Vec<(f64, f64, f64)>,
113    /// Join the last point to the first.
114    pub closed: bool,
115    /// Full line width in pixels.
116    pub width_px: f64,
117}
118
119impl NewVectorLine {
120    /// The points without the width factors.
121    pub fn xy(&self) -> Vec<(f64, f64)> {
122        self.points.iter().map(|&(x, y, _)| (x, y)).collect()
123    }
124}
125
126/// One panel of a コマ枠フォルダー: its outline (layer pixels) and the
127/// border's full width.
128#[derive(Debug, Clone, PartialEq)]
129pub struct NewFrame {
130    /// Outline of the panel, in layer pixels.
131    pub points: Vec<(f64, f64)>,
132    /// Full width of the border in pixels.
133    pub width_px: f64,
134}
135
136/// The layout of one コマ枠フォルダー border, taken from a CSP-made frame
137/// folder: its header and first point, whose undecoded fields new borders
138/// keep (header 96 bytes and points 88 bytes in CSP 5).
139#[derive(Debug, Clone, PartialEq)]
140pub struct FrameTemplate {
141    header: Vec<u8>,
142    point: Vec<u8>,
143}
144
145impl FrameTemplate {
146    /// The first border of a frame folder's vector data, if it is in the
147    /// known layout.
148    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    /// Brush style id the borders are drawn with.
157    pub fn brush_style(&self) -> u32 {
158        be_u32_at(&self.header, HEADER_BRUSH_STYLE)
159    }
160}
161
162impl VectorObject {
163    /// The brush style id (`BrushStyle.MainId`) a new object copying this
164    /// one should use: the only id of an 88-byte header, the one at +76 of
165    /// larger headers (the second; the first of a 92-byte header can be 0).
166    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    /// A black line drawn with brush style `brush_style`. None for lines
175    /// with fewer than two points.
176    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    /// A closed frame border laid out like `template`, drawn with brush
209    /// style `brush_style`. None for outlines with fewer than three points.
210    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
239/// Outward unit bisector and miter length of each corner of `points`.
240fn 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    // Signed area: which side of each edge is inside.
247    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            // Outward normals of the two edges (y points down, as in CSP).
260            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            // The corner's half angle: miter = r / cos(half the turn).
265            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    /// Vector data holding `lines` (see [`VectorObject::new_line`]).
273    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    /// A frame folder's vector data holding `frames` (see
279    /// [`VectorObject::new_frame`]).
280    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    /// The brush style of the first object (see [`VectorObject::brush_style`]).
286    pub fn brush_style(&self) -> Option<u32> {
287        self.objects.first()?.brush_style()
288    }
289}
290
291/// A short fingerprint of lines' shapes (points to the pixel, in order), to
292/// tell whether a vector layer still holds what was written into it.
293pub fn shape_signature<'a>(lines: impl IntoIterator<Item = &'a [(f64, f64)]>) -> String {
294    // FNV-1a: the same on every build, unlike the standard hasher.
295    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        // The last point of an open line starts no segment.
337        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        // A right-angled corner: miter r·√2 along the outward diagonal.
377        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}