Skip to main content

graph_explorer_render/
surface.rs

1//! Surface sizing rules.
2//!
3//! Lives here rather than in `graph-explorer-wasm` so it is testable without
4//! wasm — the wasm crate is the wasm-bindgen boundary and carries no tests.
5
6/// Shrink `(width, height)` until neither axis exceeds `max`, preserving the
7/// aspect ratio. Sizes already within the limit are returned untouched.
8///
9/// `wgpu` treats a surface larger than `max_texture_dimension_2d` as a
10/// validation error, and on wasm a validation error is a panic that takes the
11/// whole instance with it — a black canvas and a dead engine, not a degraded
12/// one. Every path that hands a size to `Surface::configure` must come through
13/// here first.
14///
15/// Both axes scale by the same factor on purpose. Clamping only the offending
16/// one would change the aspect ratio of the drawing buffer while the CSS box
17/// kept its shape, and every circle on screen would render as an ellipse.
18///
19/// A zero on either axis is returned unchanged: it is separately invalid, and
20/// silently inventing a size would hide that from the caller who must reject it.
21pub fn fit_within_max_dimension(width: u32, height: u32, max: u32) -> (u32, u32) {
22    if width == 0 || height == 0 || max == 0 {
23        return (width, height);
24    }
25    if width <= max && height <= max {
26        return (width, height);
27    }
28    let scale = (max as f64 / width as f64).min(max as f64 / height as f64);
29    // `max(1)` guards the degenerate case of an extreme aspect ratio scaling
30    // the short axis to nothing; a 1px axis is poor but valid, a 0px one is not.
31    let w = ((width as f64 * scale).floor() as u32).clamp(1, max);
32    let h = ((height as f64 * scale).floor() as u32).clamp(1, max);
33    (w, h)
34}
35
36#[cfg(test)]
37mod tests {
38    use super::*;
39
40    #[test]
41    fn sizes_within_the_limit_pass_through_untouched() {
42        assert_eq!(fit_within_max_dimension(1280, 720, 2048), (1280, 720));
43        assert_eq!(fit_within_max_dimension(2048, 2048, 2048), (2048, 2048));
44    }
45
46    #[test]
47    fn oversized_widths_shrink_to_the_limit() {
48        // The case that killed the engine live: a 1280x720 CSS canvas rendered
49        // at devicePixelRatio 2 against wgpu's downlevel WebGL2 cap of 2048.
50        assert_eq!(fit_within_max_dimension(2560, 1440, 2048), (2048, 1152));
51    }
52
53    #[test]
54    fn aspect_ratio_survives_the_clamp() {
55        // Nodes are drawn as circles by a shader that assumes square pixels;
56        // if this ratio drifted they would render as ellipses.
57        for (w, h) in [(2560u32, 1440u32), (3840, 2160), (5000, 1000), (999, 4001)] {
58            let (cw, ch) = fit_within_max_dimension(w, h, 2048);
59            let before = w as f64 / h as f64;
60            let after = cw as f64 / ch as f64;
61            assert!(
62                (before - after).abs() / before < 0.01,
63                "{w}x{h} -> {cw}x{ch}: {before} vs {after}",
64            );
65        }
66    }
67
68    #[test]
69    fn both_axes_end_up_within_the_limit() {
70        for (w, h) in [(4096u32, 4096u32), (8000, 100), (100, 8000), (2049, 2049)] {
71            let (cw, ch) = fit_within_max_dimension(w, h, 2048);
72            assert!(cw <= 2048 && ch <= 2048, "{w}x{h} -> {cw}x{ch}");
73            assert!(cw >= 1 && ch >= 1, "{w}x{h} -> {cw}x{ch} collapsed an axis");
74        }
75    }
76
77    #[test]
78    fn an_extreme_aspect_ratio_never_collapses_an_axis_to_zero() {
79        // Scaling 100000x1 to fit 2048 would put the short axis at 0.02px.
80        let (w, h) = fit_within_max_dimension(100_000, 1, 2048);
81        assert!(w <= 2048 && h >= 1, "got {w}x{h}");
82    }
83
84    #[test]
85    fn zero_sizes_are_passed_through_for_the_caller_to_reject() {
86        assert_eq!(fit_within_max_dimension(0, 720, 2048), (0, 720));
87        assert_eq!(fit_within_max_dimension(1280, 0, 2048), (1280, 0));
88    }
89}