Artboard Options: Presentation Mode draws no fade, and the fade's rectangles stay few for many artboards

The fade outside the artboards is no longer drawn in Presentation Mode, which shows only the art, even when the Artboard tool is active.

The rectangles that make up the fade are now merged down from band to band when a gap keeps the same left and right edges, and the bands are swept with the boards sorted by their top edge. A row of 200 top-aligned artboards of different heights now gives at most 600 rectangles instead of about 20,000.
This commit is contained in:
dotCooCoo
2026-11-08 12:47:55 +07:00
committed by bflatastic
parent e620da7ed6
commit 92d3505529
2 changed files with 81 additions and 13 deletions
+48 -13
View File
@@ -832,10 +832,12 @@ fn reproject_frame(app: &VectorcraftApp, painter: &egui::Painter, xf: &Xf, rect:
/// Does the area outside the artboards draw darker? Artboard Options › Fade Region Outside Artboard
/// darkens it while the Artboard tool is active, and with Update While Dragging off it draws as
/// usual while the tool moves or resizes an artboard. Exports and prints never draw the fade.
/// usual while the tool moves or resizes an artboard. Presentation Mode, exports and prints never
/// draw the fade.
fn fades_outside_artboards(app: &VectorcraftApp) -> bool {
let prefs = &app.session.prefs;
app.session.tool_id() == "artboard"
app.ui.screen_mode != 3
&& app.session.tool_id() == "artboard"
&& prefs.fade_region_outside_artboard
&& (prefs.artboard_update_while_dragging || !app.session.tool_transforming())
}
@@ -861,29 +863,62 @@ fn fade_outside_artboards(painter: &egui::Painter, xf: &Xf, doc: &vectorcraft_do
}
/// The parts of `bounds` that no rectangle of `boards` covers, as rectangles that do not overlap:
/// for each band between consecutive top and bottom edges, the spans between the boards that
/// cross it.
/// for each band between consecutive top and bottom edges, the gaps between the boards that cross
/// it. A gap with the same left and right edges as a gap of the band above extends that gap's
/// rectangle down, so each board edge adds at most a few rectangles.
fn outside_rects(bounds: Rect, boards: &[Rect]) -> Vec<Rect> {
let mut ys: Vec<f64> = boards.iter().flat_map(|b| [b.y0, b.y1]).filter(|y| *y > bounds.y0 && *y < bounds.y1).collect();
ys.extend([bounds.y0, bounds.y1]);
ys.sort_by(f64::total_cmp);
ys.dedup();
let mut out = vec![];
let mut by_top: Vec<Rect> = boards.iter().copied().filter(|b| b.y0 < bounds.y1 && b.y1 > bounds.y0).collect();
by_top.sort_by(|a, b| a.y0.total_cmp(&b.y0));
let mut next = 0;
// The boards that cross the current band, left to right.
let mut active: Vec<Rect> = vec![];
let mut out: Vec<Rect> = vec![];
// The indices in `out` of the band above's rectangles, left to right.
let mut above: Vec<usize> = vec![];
for band in ys.windows(2) {
let &[y0, y1] = band else { continue };
let mut spans: Vec<(f64, f64)> =
boards.iter().filter(|b| b.y0 <= y0 && b.y1 >= y1).map(|b| (b.x0.max(bounds.x0), b.x1.min(bounds.x1))).filter(|(a, b)| a < b).collect();
spans.sort_by(|a, b| a.0.total_cmp(&b.0));
while let Some(b) = by_top.get(next).copied().filter(|b| b.y0 <= y0) {
let at = active.partition_point(|a| a.x0 <= b.x0);
active.insert(at, b);
next += 1;
}
active.retain(|b| b.y1 >= y1);
let mut gaps = vec![];
let mut x = bounds.x0;
for (a, b) in spans {
if a > x {
out.push(Rect::new(x, y0, a, y1));
for b in &active {
let (a, e) = (b.x0.max(bounds.x0), b.x1.min(bounds.x1));
if a >= e {
continue;
}
x = x.max(b);
if a > x {
gaps.push((x, a));
}
x = x.max(e);
}
if x < bounds.x1 {
out.push(Rect::new(x, y0, bounds.x1, y1));
gaps.push((x, bounds.x1));
}
let mut prev = above.iter().copied().peekable();
let mut row = Vec::with_capacity(gaps.len());
for (a, e) in gaps {
while prev.next_if(|&i| out[i].x0 < a).is_some() {}
match prev.peek().copied() {
Some(i) if out[i].x0 == a && out[i].x1 == e => {
out[i].y1 = y1;
row.push(i);
prev.next();
}
_ => {
row.push(out.len());
out.push(Rect::new(a, y0, e, y1));
}
}
}
above = row;
}
out
}
+33
View File
@@ -9156,6 +9156,39 @@ fn the_artboard_tool_fades_the_pasteboard() {
assert!(!fades(&mut app, &ctx), "the preference off");
}
/// Presentation Mode shows only the art: with the Artboard tool active and the fade on, it draws no
/// fade, and the fade comes back when Presentation Mode ends.
#[test]
fn presentation_mode_draws_no_fade() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 400, "height": 300})).unwrap();
let ctx = egui::Context::default();
frame(&mut app, &ctx, vec![]);
app.select_tool("artboard");
assert!(fades(&mut app, &ctx), "the Artboard tool");
app.run("view.presentation", json!({})).unwrap();
assert_eq!(app.ui.screen_mode, 3);
assert_eq!(app.session.tool_id(), "artboard");
assert!(!fades(&mut app, &ctx), "Presentation Mode");
app.run("view.presentation", json!({})).unwrap();
assert!(fades(&mut app, &ctx), "after Presentation Mode");
}
/// A row of boards aligned at the top, each a different height, gives a number of parts that grows
/// with the number of boards, not with its square, and the parts still add up to the uncovered area.
#[test]
fn outside_rects_of_a_row_of_boards_stay_few() {
let n = 200;
let boards: Vec<Rect> =
(0..n).map(|i| Rect::new(20.0 * f64::from(i) + 10.0, 10.0, 20.0 * f64::from(i) + 20.0, 20.0 + 10.0 * f64::from(i))).collect();
let bounds = Rect::new(0.0, 0.0, 20.0 * f64::from(n) + 10.0, 10.0 * f64::from(n) + 40.0);
let parts = outside_rects(bounds, &boards);
assert!(parts.len() <= 3 * n as usize, "{} parts for {n} boards", parts.len());
let area = |r: &Rect| r.width() * r.height();
let covered: f64 = boards.iter().map(area).sum();
assert_eq!(parts.iter().map(area).sum::<f64>(), area(&bounds) - covered);
}
/// The fade covers the art that lies on the pasteboard as well as the pasteboard: it is drawn over
/// the art, over a point of an object outside the artboard and not over a point of an object on it,
/// in an upright view and in a rotated one.