Skip to main content

cranpose_ui_graphics/
shape_records.rs

1use bytemuck::{Pod, Zeroable};
2
3use crate::ShapeRecord;
4
5/// Shape properties independent of an arc's angles, in instance-buffer layout.
6#[repr(C)]
7#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)]
8pub struct ShapeRecordBody {
9    /// The stored local rectangle.
10    pub rect: [f32; 4],
11    /// The solid colour or first gradient stop.
12    pub color: [f32; 4],
13    /// The stroke width.
14    pub stroke_width: f32,
15    /// Packed shape, stroke, blend and arc facts from [`ShapeRecord::flags`].
16    pub flags: u32,
17    /// Zero for a solid brush, otherwise one plus its table index.
18    pub brush: u32,
19    /// The placement index when a renderer combines recordings.
20    pub placement: u32,
21    /// Arc centre x and y, followed by normalised inner and outer radii.
22    pub arc_geometry: [f32; 4],
23}
24
25/// Corner radii and arc-angle properties, in instance-buffer layout.
26#[repr(C)]
27#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)]
28pub struct ShapeRecordCurve {
29    /// Corner radii for rectangles; zero for bands, whose trig the GPU
30    /// derives from `arc_normalized` once per uploaded band and writes here.
31    /// No other record's row equals the row of a band that draws: such a
32    /// band's sweep, `arc_normalized[1]`, is positive and its radii and
33    /// `arc_normalized[2..]` zero, while a rect's and an unbanded round
34    /// rect's `arc_normalized` is zero, a banded ring's radii are positive,
35    /// a line of positive length carries its half length in
36    /// `arc_normalized[2]` and one of no length points along +X. A record
37    /// that becomes or stops being a band therefore uploads a row of its
38    /// own, and a band whose row is unchanged keeps its trig.
39    pub radii: [f32; 4],
40    /// A band's normalised start and sweep, the rest zero; a line's unit
41    /// direction and half length.
42    pub arc_normalized: [f32; 4],
43}
44
45/// A record's original line or arc arguments, kept on the CPU only for the
46/// records that have any.
47#[repr(C)]
48#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)]
49struct RecordSource {
50    index: u32,
51    arguments: [f32; 4],
52}
53
54/// Recorded shapes stored in parallel columns, ready for GPU upload.
55///
56/// Angle changes leave the body column intact. Original line and arc
57/// arguments remain on the CPU so materialisation preserves exactly what
58/// the caller supplied; the rects, round rects and path slices that make up
59/// most recordings have none, and keep no column for them.
60#[derive(Clone, Debug, Default, PartialEq)]
61pub struct ShapeRecords {
62    bodies: Vec<ShapeRecordBody>,
63    curves: Vec<ShapeRecordCurve>,
64    /// By record index.
65    sources: Vec<RecordSource>,
66}
67
68impl ShapeRecords {
69    pub(crate) fn with_capacity(capacity: usize) -> Self {
70        Self {
71            bodies: Vec::with_capacity(capacity),
72            curves: Vec::with_capacity(capacity),
73            sources: Vec::new(),
74        }
75    }
76
77    /// The number of recorded shapes.
78    pub fn len(&self) -> usize {
79        self.bodies.len()
80    }
81
82    /// Whether the recording has no shapes.
83    pub fn is_empty(&self) -> bool {
84        self.bodies.is_empty()
85    }
86
87    /// The angle-independent column, directly usable as GPU instance data.
88    pub fn bodies(&self) -> &[ShapeRecordBody] {
89        &self.bodies
90    }
91
92    /// The radius and angle column, directly usable as GPU instance data.
93    pub fn curves(&self) -> &[ShapeRecordCurve] {
94        &self.curves
95    }
96
97    /// Whether the record at `index` is an opaque fill whose interior is worth
98    /// laying down ahead of later paint: the test that marks a segment's
99    /// [`occluders`](crate::RecordSegment::occluders). `false` past the end.
100    pub fn occludes(&self, index: usize) -> bool {
101        self.bodies
102            .get(index)
103            .zip(self.curves.get(index))
104            .is_some_and(|(body, curve)| crate::record::interior_occludes(body, curve))
105    }
106
107    /// Reconstructs one complete record, including the original arc arguments.
108    pub fn get(&self, index: usize) -> Option<ShapeRecord> {
109        self.bodies
110            .get(index)
111            .map(|body| reconstruct(body, &self.curves[index], self.source(index)))
112    }
113
114    /// Iterates over complete records in draw order without allocating.
115    pub fn iter(&self) -> impl ExactSizeIterator<Item = ShapeRecord> + DoubleEndedIterator + '_ {
116        self.bodies
117            .iter()
118            .zip(&self.curves)
119            .enumerate()
120            .map(|(index, (body, curve))| reconstruct(body, curve, self.source(index)))
121    }
122
123    /// The original arguments of record `index`: zeros for a record kept
124    /// without any.
125    fn source(&self, index: usize) -> [f32; 4] {
126        let Ok(index) = u32::try_from(index) else {
127            return [0.0; 4];
128        };
129        self.sources
130            .binary_search_by_key(&index, |source| source.index)
131            .map_or([0.0; 4], |found| self.sources[found].arguments)
132    }
133
134    pub(crate) fn capacity(&self) -> usize {
135        self.bodies.capacity().min(self.curves.capacity())
136    }
137
138    pub(crate) fn heap_bytes(&self) -> usize {
139        self.bodies.capacity() * std::mem::size_of::<ShapeRecordBody>()
140            + self.curves.capacity() * std::mem::size_of::<ShapeRecordCurve>()
141            + self.sources.capacity() * std::mem::size_of::<RecordSource>()
142    }
143
144    pub(crate) fn source_bytes(&self) -> &[u8] {
145        bytemuck::cast_slice(&self.sources)
146    }
147
148    pub(crate) fn clear(&mut self) {
149        self.bodies.clear();
150        self.curves.clear();
151        self.sources.clear();
152    }
153
154    pub(crate) fn trim_growth(&mut self) {
155        trim_growth(&mut self.bodies);
156        trim_growth(&mut self.curves);
157        trim_growth(&mut self.sources);
158    }
159
160    pub(crate) fn reserve(&mut self, additional: usize) {
161        self.bodies.reserve(additional);
162        self.curves.reserve(additional);
163    }
164
165    pub(crate) fn push(
166        &mut self,
167        body: ShapeRecordBody,
168        curve: ShapeRecordCurve,
169        source: [f32; 4],
170    ) {
171        if source.iter().any(|argument| argument.to_bits() != 0)
172            && let Ok(index) = u32::try_from(self.bodies.len())
173        {
174            self.sources.push(RecordSource {
175                index,
176                arguments: source,
177            });
178        }
179        self.bodies.push(body);
180        self.curves.push(curve);
181    }
182}
183
184/// Gives back the room past an eighth more than `items` holds once growth
185/// left over a quarter more: a recording drawn again keeps its capacity,
186/// so room its first growth left would stay for as long as its node.
187pub(crate) fn trim_growth<T>(items: &mut Vec<T>) {
188    let len = items.len();
189    if items.capacity() > len + len / 4 {
190        items.shrink_to(len + len / 8);
191    }
192}
193
194fn reconstruct(body: &ShapeRecordBody, curve: &ShapeRecordCurve, source: [f32; 4]) -> ShapeRecord {
195    ShapeRecord {
196        rect: body.rect,
197        radii: curve.radii,
198        color: body.color,
199        stroke_width: body.stroke_width,
200        flags: body.flags,
201        brush: body.brush,
202        reserved: body.placement,
203        arc: [
204            body.arc_geometry[0],
205            body.arc_geometry[1],
206            source[0],
207            source[1],
208        ],
209        arc_band: [
210            source[2],
211            source[3],
212            body.arc_geometry[2],
213            body.arc_geometry[3],
214        ],
215        arc_normalized: curve.arc_normalized,
216    }
217}
218
219#[cfg(test)]
220#[path = "tests/shape_records_tests.rs"]
221mod tests;