1use crate::Color;
10use crate::convert::to_ox_point;
11use oxideav_core::{
12 Group, Node, Paint, Path, PathNode, Point as OxPoint, Rgba, Stroke, VectorFrame, ViewBox,
13};
14use stipple_geometry::{Point, Rect, Size};
15
16const KAPPA: f64 = 0.552_284_749_830_793_4;
19
20#[derive(Clone, Debug, PartialEq)]
25pub enum DrawCmd {
26 Rect {
29 rect: Rect,
30 color: Color,
31 radius: f64,
32 border: f64,
33 },
34 Text {
36 text: String,
37 origin: Point,
38 size: f64,
39 color: Color,
40 },
41 PushClip(Rect),
46 PopClip,
48 Viewport { rect: Rect, id: u64 },
55}
56
57#[derive(Clone, Debug)]
61struct ClipFrame {
62 clip: Option<Path>,
63 nodes: Vec<Node>,
64}
65
66#[derive(Clone, Debug)]
68pub struct Scene {
69 logical_size: Size,
70 stack: Vec<ClipFrame>,
74 commands: Vec<DrawCmd>,
75}
76
77impl Scene {
78 pub fn new(logical_size: Size) -> Self {
80 Self {
81 logical_size,
82 stack: vec![ClipFrame {
83 clip: None,
84 nodes: Vec::new(),
85 }],
86 commands: Vec::new(),
87 }
88 }
89
90 fn emit(&mut self, node: Node) {
92 self.stack.last_mut().unwrap().nodes.push(node);
94 }
95
96 pub fn push_clip(&mut self, rect: Rect) {
100 self.stack.push(ClipFrame {
101 clip: Some(rect_path(rect)),
102 nodes: Vec::new(),
103 });
104 self.commands.push(DrawCmd::PushClip(rect));
105 }
106
107 pub fn pop_clip(&mut self) {
110 if self.stack.len() <= 1 {
111 return; }
113 let frame = self.stack.pop().unwrap();
114 let group = Group {
115 children: frame.nodes,
116 clip: frame.clip,
117 ..Group::new()
118 };
119 self.emit(Node::Group(group));
120 self.commands.push(DrawCmd::PopClip);
121 }
122
123 pub fn commands(&self) -> &[DrawCmd] {
126 &self.commands
127 }
128
129 #[inline]
131 pub fn logical_size(&self) -> Size {
132 self.logical_size
133 }
134
135 #[inline]
137 pub fn len(&self) -> usize {
138 self.stack[0].nodes.len()
139 }
140
141 #[inline]
142 pub fn is_empty(&self) -> bool {
143 self.stack.iter().all(|f| f.nodes.is_empty())
144 }
145
146 pub fn fill_rect(&mut self, rect: Rect, color: Color) {
148 let path = rect_path(rect);
149 self.emit(Node::Path(
150 PathNode::new(path).with_fill(Paint::Solid(color.into())),
151 ));
152 self.commands.push(DrawCmd::Rect {
153 rect,
154 color,
155 radius: 0.0,
156 border: 0.0,
157 });
158 }
159
160 pub fn fill_round_rect(&mut self, rect: Rect, radius: f64, color: Color) {
163 let path = round_rect_path(rect, radius);
164 self.emit(Node::Path(
165 PathNode::new(path).with_fill(Paint::Solid(color.into())),
166 ));
167 self.commands.push(DrawCmd::Rect {
168 rect,
169 color,
170 radius,
171 border: 0.0,
172 });
173 }
174
175 pub fn stroke_rect(&mut self, rect: Rect, color: Color, width: f64) {
177 let path = rect_path(rect);
178 let stroke = Stroke::solid(width as f32, Rgba::from(color));
179 self.emit(Node::Path(PathNode::new(path).with_stroke(stroke)));
180 self.commands.push(DrawCmd::Rect {
181 rect,
182 color,
183 radius: 0.0,
184 border: width,
185 });
186 }
187
188 pub fn fill_viewport(&mut self, rect: Rect, id: u64, placeholder: Color) {
194 let path = rect_path(rect);
195 self.emit(Node::Path(
196 PathNode::new(path).with_fill(Paint::Solid(placeholder.into())),
197 ));
198 self.commands.push(DrawCmd::Viewport { rect, id });
199 }
200
201 pub(crate) fn record_text(&mut self, text: &str, origin: Point, size: f64, color: Color) {
204 self.commands.push(DrawCmd::Text {
205 text: text.to_string(),
206 origin,
207 size,
208 color,
209 });
210 }
211
212 pub fn push_node(&mut self, node: Node) {
216 self.emit(node);
217 }
218
219 pub fn into_vector_frame(self) -> VectorFrame {
225 let size = self.logical_size;
226 self.into_vector_frame_view(Rect::from_xywh(0.0, 0.0, size.width, size.height))
227 }
228
229 pub fn into_vector_frame_region(self, view: Rect) -> VectorFrame {
234 self.into_vector_frame_view(view)
235 }
236
237 fn into_vector_frame_view(mut self, view: Rect) -> VectorFrame {
240 while self.stack.len() > 1 {
242 self.pop_clip();
243 }
244 let (w, h) = (view.width() as f32, view.height() as f32);
245 let root = Group {
246 children: self.stack.pop().unwrap().nodes,
247 ..Group::new()
248 };
249 VectorFrame::new(w, h)
250 .with_view_box(ViewBox {
251 min_x: view.min_x() as f32,
252 min_y: view.min_y() as f32,
253 width: w,
254 height: h,
255 })
256 .with_root(root)
257 }
258}
259
260fn rect_path(rect: Rect) -> Path {
261 let (x0, y0, x1, y1) = (rect.min_x(), rect.min_y(), rect.max_x(), rect.max_y());
262 let mut p = Path::new();
263 p.move_to(OxPoint {
264 x: x0 as f32,
265 y: y0 as f32,
266 });
267 p.line_to(OxPoint {
268 x: x1 as f32,
269 y: y0 as f32,
270 });
271 p.line_to(OxPoint {
272 x: x1 as f32,
273 y: y1 as f32,
274 });
275 p.line_to(OxPoint {
276 x: x0 as f32,
277 y: y1 as f32,
278 });
279 p.close();
280 p
281}
282
283fn round_rect_path(rect: Rect, radius: f64) -> Path {
284 let r = radius.max(0.0).min(rect.width().min(rect.height()) / 2.0);
285 if r <= 0.0 {
286 return rect_path(rect);
287 }
288 let (x0, y0, x1, y1) = (rect.min_x(), rect.min_y(), rect.max_x(), rect.max_y());
289 let k = r * KAPPA;
290 use stipple_geometry::Point as P;
291 let mut p = Path::new();
292 p.move_to(to_ox_point(P::new(x0 + r, y0)));
294 p.line_to(to_ox_point(P::new(x1 - r, y0)));
295 p.cubic_to(
296 to_ox_point(P::new(x1 - r + k, y0)),
297 to_ox_point(P::new(x1, y0 + r - k)),
298 to_ox_point(P::new(x1, y0 + r)),
299 );
300 p.line_to(to_ox_point(P::new(x1, y1 - r)));
301 p.cubic_to(
302 to_ox_point(P::new(x1, y1 - r + k)),
303 to_ox_point(P::new(x1 - r + k, y1)),
304 to_ox_point(P::new(x1 - r, y1)),
305 );
306 p.line_to(to_ox_point(P::new(x0 + r, y1)));
307 p.cubic_to(
308 to_ox_point(P::new(x0 + r - k, y1)),
309 to_ox_point(P::new(x0, y1 - r + k)),
310 to_ox_point(P::new(x0, y1 - r)),
311 );
312 p.line_to(to_ox_point(P::new(x0, y0 + r)));
313 p.cubic_to(
314 to_ox_point(P::new(x0, y0 + r - k)),
315 to_ox_point(P::new(x0 + r - k, y0)),
316 to_ox_point(P::new(x0 + r, y0)),
317 );
318 p.close();
319 p
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325 use stipple_geometry::Point;
326
327 #[test]
328 fn scene_lowers_to_frame_with_viewbox() {
329 let mut scene = Scene::new(Size::new(200.0, 100.0));
330 scene.fill_rect(Rect::from_xywh(10.0, 10.0, 50.0, 50.0), Color::WHITE);
331 assert_eq!(scene.len(), 1);
332 let frame = scene.into_vector_frame();
333 assert_eq!((frame.width, frame.height), (200.0, 100.0));
334 let vb = frame.view_box.expect("view box set");
335 assert_eq!((vb.width, vb.height), (200.0, 100.0));
336 assert_eq!(frame.root.children.len(), 1);
337 }
338
339 #[test]
340 fn records_structured_draw_commands() {
341 let mut scene = Scene::new(Size::new(100.0, 100.0));
342 scene.fill_rect(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::WHITE);
343 scene.fill_round_rect(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), 4.0, Color::BLACK);
344 scene.stroke_rect(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::WHITE, 2.0);
345 let cmds = scene.commands();
346 assert_eq!(cmds.len(), 3);
347 assert!(
348 matches!(cmds[0], DrawCmd::Rect { radius, border, .. } if radius == 0.0 && border == 0.0)
349 );
350 assert!(matches!(cmds[1], DrawCmd::Rect { radius, .. } if radius == 4.0));
351 assert!(matches!(cmds[2], DrawCmd::Rect { border, .. } if border == 2.0));
352 }
353
354 #[test]
355 fn push_pop_clip_nests_a_clipped_group() {
356 let mut scene = Scene::new(Size::new(200.0, 200.0));
357 scene.fill_rect(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::WHITE); scene.push_clip(Rect::from_xywh(20.0, 20.0, 50.0, 50.0));
359 scene.fill_rect(Rect::from_xywh(25.0, 25.0, 100.0, 100.0), Color::BLACK); scene.fill_rect(Rect::from_xywh(30.0, 30.0, 5.0, 5.0), Color::BLACK); scene.pop_clip();
362 assert_eq!(scene.len(), 2);
364 let cmds = scene.commands();
366 assert!(matches!(cmds[1], DrawCmd::PushClip(_)));
367 assert!(matches!(cmds[4], DrawCmd::PopClip));
368 let frame = scene.into_vector_frame();
369 assert_eq!(frame.root.children.len(), 2);
371 let clipped = match &frame.root.children[1] {
372 Node::Group(g) => g,
373 _ => panic!("expected a clipped group as the second child"),
374 };
375 assert!(clipped.clip.is_some(), "nested group carries the clip path");
376 assert_eq!(clipped.children.len(), 2, "two clipped rects");
377 }
378
379 #[test]
380 fn fill_viewport_paints_placeholder_and_records_command() {
381 let mut scene = Scene::new(Size::new(200.0, 100.0));
382 let r = Rect::from_xywh(10.0, 10.0, 80.0, 60.0);
383 scene.fill_viewport(r, 7, Color::rgb(0x20, 0x24, 0x2c));
384 assert_eq!(scene.len(), 1);
386 let cmds = scene.commands();
388 assert_eq!(cmds.len(), 1);
389 assert!(matches!(cmds[0], DrawCmd::Viewport { id, rect } if id == 7 && rect == r));
390 }
391
392 #[test]
393 fn zero_radius_round_rect_is_plain_rect() {
394 let path = round_rect_path(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), 0.0);
395 assert_eq!(path.commands.len(), 5);
397 }
398
399 #[test]
400 fn round_rect_has_corner_curves() {
401 let path = round_rect_path(Rect::from_xywh(0.0, 0.0, 20.0, 20.0), 4.0);
402 let cubics = path
403 .commands
404 .iter()
405 .filter(|c| matches!(c, oxideav_core::PathCommand::CubicCurveTo { .. }))
406 .count();
407 assert_eq!(cubics, 4);
408 let _ = Point::ORIGIN;
409 }
410}