1use deckcraft_geom::Xfrm;
11use deckcraft_model::resolve::{self, Ctx};
12use deckcraft_model::style::{Effects, Line};
13use deckcraft_model::{Presentation, Shape, ShapeKind};
14use kurbo::Rect;
15
16use crate::MAX_SIDE;
17
18const MAX_SPILL: f64 = 2000.0;
21
22pub const TILE: i64 = 256;
25
26pub(crate) fn spill(line: Option<&Line>, effects: Option<&Effects>, w: f64, h: f64) -> f64 {
29 let fin = |v: f64| if v.is_finite() { v.abs() } else { 0.0 };
30 let mut s: f64 = 0.0;
31 if let Some(l) = line
32 && l.fill.as_ref().is_some_and(|f| !f.is_none())
33 {
34 s = s.max(fin(l.width.unwrap_or(0.75)) * 3.0 + 1.0);
36 }
37 if let Some(e) = effects {
38 let big = fin(w).max(fin(h));
39 if let Some(sh) = &e.outer_shadow {
40 let grow = (fin(sh.sx).max(fin(sh.sy)) - 1.0).max(0.0) * big;
41 s = s.max(fin(sh.dist) + fin(sh.blur) + grow);
42 }
43 if let Some(g) = &e.glow {
44 s = s.max(fin(g.radius));
45 }
46 if let Some(r) = &e.reflection {
47 s = s.max(fin(h) + fin(r.dist));
48 }
49 }
50 s.min(MAX_SPILL)
51}
52
53fn shape_box(rctx: &Ctx, s: &Shape) -> Option<Rect> {
55 if s.hidden {
56 return None;
57 }
58 let x: Xfrm = resolve::xfrm(rctx, s);
59 if !x.is_finite() {
60 return None;
61 }
62 let b = match &s.kind {
63 ShapeKind::Ink { strokes } => {
64 let mut pts = strokes
65 .iter()
66 .flat_map(|k| k.points.iter())
67 .filter(|(px, py, _)| px.is_finite() && py.is_finite());
68 let (px, py, _) = pts.next()?;
69 let r = pts.fold(Rect::new(*px, *py, *px, *py), |r, (px, py, _)| {
70 r.union_pt(kurbo::Point::new(*px, *py))
71 });
72 let w = strokes
73 .iter()
74 .map(|k| k.width)
75 .filter(|w| w.is_finite())
76 .fold(0.0_f64, f64::max)
77 .min(MAX_SPILL);
78 return Some(r.inflate(w, w));
79 }
80 _ => x.bounds(),
81 };
82 let (line, _) = resolve::line(rctx, s);
83 let (fx, _) = resolve::effects(rctx, s);
84 let d = spill(Some(&line), fx.as_ref(), x.w, x.h);
85 Some(b.inflate(d, d))
86}
87
88fn union_boxes<'a>(rctx: &Ctx, shapes: impl IntoIterator<Item = &'a Shape>, into: Rect) -> Rect {
89 shapes
90 .into_iter()
91 .filter_map(|s| shape_box(rctx, s))
92 .filter(|r| r.is_finite())
93 .fold(into, |a, r| a.union(r))
94}
95
96fn slide_rect(pres: &Presentation) -> Rect {
97 Rect::new(0.0, 0.0, pres.slide_size.width, pres.slide_size.height)
98}
99
100pub fn slide_extent(pres: &Presentation, index: usize) -> Rect {
104 let page = slide_rect(pres);
105 let Some(slide) = pres.slides.get(index) else {
106 return page;
107 };
108 let Some(rctx) = Ctx::for_slide(pres, slide) else {
109 return page;
110 };
111 let mut r = union_boxes(&rctx, &slide.shapes, page);
112 let (show_layout, show_master) = resolve::show_master_shapes(slide, rctx.layout);
113 if show_master {
114 let mctx = Ctx::for_master(pres, rctx.master);
115 r = union_boxes(
116 &mctx,
117 rctx.master.shapes.iter().filter(|s| s.ph.is_none()),
118 r,
119 );
120 }
121 if show_layout && let Some(l) = rctx.layout {
122 let lctx = Ctx::for_layout(pres, rctx.master, l);
123 r = union_boxes(&lctx, l.shapes.iter().filter(|s| s.ph.is_none()), r);
124 }
125 r
126}
127
128pub fn layout_extent(pres: &Presentation, master: usize, layout: Option<usize>) -> Rect {
130 let page = slide_rect(pres);
131 let Some(m) = pres.masters.get(master) else {
132 return page;
133 };
134 match layout.and_then(|i| m.layouts.get(i)) {
135 Some(l) => {
136 let mut r = union_boxes(&Ctx::for_layout(pres, m, l), &l.shapes, page);
137 if l.show_master_shapes {
138 r = union_boxes(
139 &Ctx::for_master(pres, m),
140 m.shapes.iter().filter(|s| s.ph.is_none()),
141 r,
142 );
143 }
144 r
145 }
146 None => union_boxes(&Ctx::for_master(pres, m), &m.shapes, page),
147 }
148}
149
150#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
152pub struct Region {
153 pub x: i32,
154 pub y: i32,
155 pub w: u32,
156 pub h: u32,
157}
158
159impl Region {
160 pub fn slide(w: u32, h: u32) -> Self {
162 Region {
163 x: 0,
164 y: 0,
165 w: w.max(1),
166 h: h.max(1),
167 }
168 }
169
170 fn edges(&self) -> (i64, i64, i64, i64) {
171 let (x, y) = (self.x as i64, self.y as i64);
172 (x, y, x + self.w as i64, y + self.h as i64)
173 }
174
175 fn from_edges(x0: i64, y0: i64, x1: i64, y1: i64) -> Self {
176 let clamp = |v: i64| v.clamp(i32::MIN as i64, i32::MAX as i64) as i32;
177 let side = |a: i64, b: i64| (b - a).clamp(1, MAX_SIDE as i64) as u32;
178 Region {
179 x: clamp(x0),
180 y: clamp(y0),
181 w: side(x0, x1),
182 h: side(y0, y1),
183 }
184 }
185
186 pub fn contains(&self, other: &Region) -> bool {
188 let (a, b) = (self.edges(), other.edges());
189 a.0 <= b.0 && a.1 <= b.1 && a.2 >= b.2 && a.3 >= b.3
190 }
191
192 pub fn beyond(&self, slide: (u32, u32)) -> bool {
194 *self != Region::slide(slide.0, slide.1)
195 }
196
197 pub fn offset(&self) -> (f64, f64) {
199 (-(self.x as f64), -(self.y as f64))
200 }
201
202 pub fn size(&self) -> (u32, u32) {
204 (self.w, self.h)
205 }
206}
207
208fn to_px(r: Rect, scale: f64) -> Option<(i64, i64, i64, i64)> {
210 let lim = i32::MAX as f64 / 2.0;
211 let px = |v: f64| {
212 if v.is_finite() {
213 Some(v.clamp(-lim, lim))
214 } else {
215 None
216 }
217 };
218 Some((
219 px((r.x0 * scale).floor())? as i64,
220 px((r.y0 * scale).floor())? as i64,
221 px((r.x1 * scale).ceil())? as i64,
222 px((r.y1 * scale).ceil())? as i64,
223 ))
224}
225
226pub fn pasteboard_region(
231 slide_px: (u32, u32),
232 scale: f64,
233 extent: Rect,
234 visible: Region,
235) -> Region {
236 let page = Region::slide(slide_px.0, slide_px.1);
237 if !(scale.is_finite() && scale > 0.0) {
238 return page;
239 }
240 let Some((ex0, ey0, ex1, ey1)) = to_px(extent, scale) else {
241 return page;
242 };
243 let (vx0, vy0, vx1, vy1) = visible.edges();
244 let (vx0, vy0) = (vx0.div_euclid(TILE) * TILE, vy0.div_euclid(TILE) * TILE);
245 let (vx1, vy1) = (
246 (vx1 + TILE - 1).div_euclid(TILE) * TILE,
247 (vy1 + TILE - 1).div_euclid(TILE) * TILE,
248 );
249 let (cx0, cy0, cx1, cy1) = (ex0.max(vx0), ey0.max(vy0), ex1.min(vx1), ey1.min(vy1));
250 if cx0 >= cx1 || cy0 >= cy1 {
251 return page;
252 }
253 let (px0, py0, px1, py1) = page.edges();
254 Region::from_edges(cx0.min(px0), cy0.min(py0), cx1.max(px1), cy1.max(py1))
255}
256
257#[cfg(test)]
258mod tests {
259 #![allow(clippy::unwrap_used, clippy::expect_used)]
260
261 use super::*;
262 use deckcraft_model::Shape;
263
264 fn black() -> deckcraft_model::ColorRef {
265 deckcraft_model::ColorRef::rgb(deckcraft_color::Rgba::BLACK)
266 }
267
268 fn deck_with(x: f64, y: f64, w: f64, h: f64) -> Presentation {
269 let mut p = Presentation::default();
270 let sl = std::sync::Arc::make_mut(&mut p.slides[0]);
271 sl.shapes.push(Shape {
272 id: deckcraft_model::ShapeId(900),
273 xfrm: Some(Xfrm::new(x, y, w, h)),
274 ..Default::default()
275 });
276 p
277 }
278
279 #[test]
280 fn a_slide_with_nothing_off_it_extends_no_further_than_the_slide() {
281 let p = deck_with(100.0, 100.0, 50.0, 50.0);
282 assert_eq!(
283 slide_extent(&p, 0),
284 Rect::new(0.0, 0.0, p.slide_size.width, p.slide_size.height)
285 );
286 }
287
288 #[test]
289 fn shapes_off_the_slide_grow_the_extent() {
290 let p = deck_with(-200.0, 50.0, 100.0, 80.0);
291 let e = slide_extent(&p, 0);
292 assert!(e.x0 <= -200.0 && e.x0 > -220.0, "{e:?}");
293 assert_eq!(e.y0, 0.0);
294 assert_eq!(e.x1, p.slide_size.width);
295 assert_eq!(
297 slide_extent(&p, 7),
298 Rect::new(0.0, 0.0, p.slide_size.width, p.slide_size.height)
299 );
300 }
301
302 #[test]
303 fn hidden_shapes_and_hostile_boxes_do_not_grow_it() {
304 let mut p = deck_with(-5000.0, 0.0, 10.0, 10.0);
305 std::sync::Arc::make_mut(&mut p.slides[0])
306 .shapes
307 .last_mut()
308 .unwrap()
309 .hidden = true;
310 let page = Rect::new(0.0, 0.0, p.slide_size.width, p.slide_size.height);
311 assert_eq!(slide_extent(&p, 0), page);
312 let p = deck_with(f64::NAN, 0.0, 10.0, 10.0);
313 assert_eq!(slide_extent(&p, 0), page);
314 let p = deck_with(f64::INFINITY, 0.0, 10.0, 10.0);
315 assert_eq!(slide_extent(&p, 0), page);
316 }
317
318 #[test]
319 fn effects_count_towards_the_extent() {
320 let mut p = deck_with(10.0, -100.0, 100.0, 50.0);
321 let sh = std::sync::Arc::make_mut(&mut p.slides[0])
322 .shapes
323 .last_mut()
324 .unwrap();
325 sh.effects = Some(Effects {
326 outer_shadow: Some(deckcraft_model::style::Shadow {
327 color: black(),
328 blur: 10.0,
329 dist: 20.0,
330 dir: 270.0,
331 inner: false,
332 sx: 1.0,
333 sy: 1.0,
334 kx: 0.0,
335 ky: 0.0,
336 align: String::new(),
337 rotate_with_shape: false,
338 }),
339 ..Default::default()
340 });
341 assert!(slide_extent(&p, 0).y0 <= -130.0);
342 assert_eq!(
343 spill(
344 None,
345 Some(&Effects {
346 glow: Some(deckcraft_model::style::Glow {
347 color: black(),
348 radius: f64::MAX
349 }),
350 ..Default::default()
351 }),
352 1.0,
353 1.0
354 ),
355 MAX_SPILL
356 );
357 }
358
359 #[test]
360 fn master_graphics_off_the_slide_count_but_not_master_placeholders() {
361 let mut p = Presentation::default();
362 let m = std::sync::Arc::make_mut(&mut p.masters[0]);
363 m.shapes.push(Shape {
364 id: deckcraft_model::ShapeId(901),
365 xfrm: Some(Xfrm::new(-150.0, 10.0, 100.0, 50.0)),
366 ..Default::default()
367 });
368 assert!(slide_extent(&p, 0).x0 <= -150.0);
369 assert!(layout_extent(&p, 0, None).x0 <= -150.0);
370 assert!(
371 layout_extent(&p, 0, Some(0)).x0 <= -150.0
372 || !p.masters[0].layouts[0].show_master_shapes
373 );
374 }
375
376 #[test]
377 fn the_region_is_the_slide_unless_something_off_it_is_in_view() {
378 let slide = (960, 540);
379 let page = Region::slide(960, 540);
380 let view = Region {
381 x: -100,
382 y: -50,
383 w: 1160,
384 h: 640,
385 };
386 let whole = Rect::new(0.0, 0.0, 960.0, 540.0);
387 assert_eq!(pasteboard_region(slide, 1.0, whole, view), page);
388 assert!(!pasteboard_region(slide, 1.0, whole, view).beyond(slide));
389 let r = pasteboard_region(slide, 1.0, Rect::new(-80.0, 0.0, 960.0, 540.0), view);
391 assert_eq!((r.x, r.y, r.w, r.h), (-80, 0, 1040, 540));
392 assert!(r.beyond(slide) && r.contains(&page));
393 assert_eq!(r.offset(), (80.0, 0.0));
394 let r = pasteboard_region(slide, 1.0, Rect::new(-5000.0, 0.0, 960.0, 540.0), view);
396 assert_eq!(r.x, -256);
397 assert_eq!(
399 pasteboard_region(
400 slide,
401 1.0,
402 Rect::new(-5000.0, 0.0, -4000.0, 540.0).union(whole),
403 Region {
404 x: 0,
405 y: 0,
406 w: 960,
407 h: 540
408 }
409 ),
410 page
411 );
412 let r = pasteboard_region((480, 270), 0.5, Rect::new(-80.0, 0.0, 960.0, 540.0), view);
414 assert_eq!(r.x, -40);
415 assert_eq!(pasteboard_region(slide, f64::NAN, whole, view), page);
416 assert_eq!(
417 pasteboard_region(slide, 1.0, Rect::new(f64::NAN, 0.0, 1.0, 1.0), view),
418 page
419 );
420 let r = pasteboard_region(
422 slide,
423 1.0,
424 Rect::new(-1e12, -1e12, 1e12, 1e12),
425 Region {
426 x: -1_000_000,
427 y: -1_000_000,
428 w: 2_000_000,
429 h: 2_000_000,
430 },
431 );
432 assert!(r.w <= MAX_SIDE && r.h <= MAX_SIDE);
433 }
434
435 #[test]
436 fn scrolling_within_a_tile_keeps_the_region() {
437 let slide = (2000, 1125);
438 let e = Rect::new(-500.0, 0.0, 960.0, 540.0);
439 let a = pasteboard_region(
440 slide,
441 2.0,
442 e,
443 Region {
444 x: -300,
445 y: 0,
446 w: 1000,
447 h: 700,
448 },
449 );
450 let b = pasteboard_region(
451 slide,
452 2.0,
453 e,
454 Region {
455 x: -290,
456 y: 10,
457 w: 1000,
458 h: 700,
459 },
460 );
461 assert_eq!(a, b);
462 }
463}