cranpose-render-wgpu 0.1.164

WGPU renderer backend for Cranpose
Documentation
use cranpose_ui_graphics::{
    ARC_BAND_MIN_RADIUS, Brush, Color, DrawScope, DrawScopeDefault, Size, Stroke, StrokeCap, TAU,
    band_class_segments,
};

use super::*;

fn inside_triangle(p: [f32; 2], a: [f32; 2], b: [f32; 2], c: [f32; 2]) -> bool {
    let sign = |p: [f32; 2], q: [f32; 2], r: [f32; 2]| {
        (p[0] - r[0]) * (q[1] - r[1]) - (q[0] - r[0]) * (p[1] - r[1])
    };
    let d1 = sign(p, a, b);
    let d2 = sign(p, b, c);
    let d3 = sign(p, c, a);
    let negative = d1 < 0.0 || d2 < 0.0 || d3 < 0.0;
    let positive = d1 > 0.0 || d2 > 0.0 || d3 > 0.0;
    !(negative && positive)
}

fn strip_covers(strip: &BandStrip, point: [f32; 2]) -> bool {
    strip
        .triangles()
        .into_iter()
        .any(|[a, b, c]| inside_triangle(point, a, b, c))
}

fn sdf_arc_band(p: [f32; 2], record: &ShapeRecord, scale: f32) -> f32 {
    let center = [record.arc[0] * scale, record.arc[1] * scale];
    let inner = record.arc_band[2] * scale;
    let outer = record.arc_band[3] * scale;
    let start = record.arc_normalized[0];
    let sweep = record.arc_normalized[1];
    let ra = (outer + inner) * 0.5;
    let rb = ((outer - inner) * 0.5).max(0.0);
    let (mid_sin, mid_cos, half_sin, half_cos) = if sweep >= TAU && start == 0.0 {
        (0.0, -1.0, 0.0, -1.0)
    } else {
        let half = sweep.clamp(0.0, TAU) * 0.5;
        let (ms, mc) = (start + half).sin_cos();
        let (hs, hc) = half.sin_cos();
        (ms, mc, hs.max(0.0), hc)
    };
    let d = [p[0] - center[0], p[1] - center[1]];
    let mut q = [
        -mid_sin * d[0] + mid_cos * d[1],
        mid_cos * d[0] + mid_sin * d[1],
    ];
    q[0] = q[0].abs();
    let mut dist = if half_cos * q[0] > half_sin * q[1] {
        let dx = q[0] - half_sin * ra;
        let dy = q[1] - half_cos * ra;
        (dx * dx + dy * dy).sqrt() - rb
    } else {
        ((q[0] * q[0] + q[1] * q[1]).sqrt() - ra).abs() - rb
    };
    let plane = half_cos * q[0] - half_sin * q[1];
    match record.band_cap() {
        StrokeCap::Butt => dist = dist.max(plane),
        StrokeCap::Square => dist = dist.max(plane - rb),
        StrokeCap::Round => {}
    }
    dist
}

fn shader_shades(record: &ShapeRecord, scale: f32, point: [f32; 2]) -> bool {
    let dist = sdf_arc_band(point, record, scale);
    let t = ((dist + 0.5).clamp(0.0, 1.0)).powi(2) * (3.0 - 2.0 * (dist + 0.5).clamp(0.0, 1.0));
    1.0 - t >= 0.001
}

fn recorded_arcs(record: impl FnOnce(&mut DrawScopeDefault)) -> Vec<ShapeRecord> {
    let mut scope = DrawScopeDefault::new(Size::new(600.0, 600.0));
    record(&mut scope);
    scope.finish().shapes().iter().collect()
}

fn assert_strip_covers_shader(record: &ShapeRecord, scale: f32) {
    let strip = BandStrip::of(record, Point::default(), scale, record.band_segments());
    let rect = record.coverage_rect();
    let left = ((rect.x * scale).floor() as i32) - 2;
    let top = ((rect.y * scale).floor() as i32) - 2;
    let right = (((rect.x + rect.width) * scale).ceil() as i32) + 2;
    let bottom = (((rect.y + rect.height) * scale).ceil() as i32) + 2;
    let mut shaded = 0usize;
    for y in top..bottom {
        for x in left..right {
            let point = [x as f32 + 0.5, y as f32 + 0.5];
            if shader_shades(record, scale, point) {
                shaded += 1;
                assert!(
                    strip_covers(&strip, point),
                    "pixel {point:?} is shaded by the arc SDF but outside the strip of {record:?}"
                );
            }
        }
    }
    assert!(shaded > 0, "the arc must shade something: {record:?}");
    let disc = quad_area(record, scale);
    assert!(
        strip.area() < disc,
        "the strip must cost less than the disc: {} vs {disc}",
        strip.area()
    );
}

#[test]
fn fill_excludes_records_outside_the_draw_window() {
    let mut scope = DrawScopeDefault::new(Size::new(100.0, 100.0));
    for width in [10.0, 30.0] {
        scope.draw_rect_at(
            cranpose_ui_graphics::Rect {
                x: 0.0,
                y: 0.0,
                width,
                height: 20.0,
            },
            Brush::solid(Color::WHITE),
        );
    }
    let recording = scope.finish();
    let selected = ShapeFill::of_draws(
        recording.tables(),
        Point::ZERO,
        1.0,
        [(1..2, Some(1))].into_iter(),
    );
    assert_eq!(selected.total(), 600.0);
    assert_eq!(selected.vertices, 4);
    let empty = ShapeFill::of_draws(
        recording.tables(),
        Point::ZERO,
        1.0,
        [(2..2, Some(1))].into_iter(),
    );
    assert_eq!(empty, ShapeFill::default());
}

#[test]
fn every_pixel_the_arc_shader_shades_lies_inside_its_strip() {
    let brush = Brush::solid(Color::WHITE);
    let records = recorded_arcs(|scope| {
        let center = Point::new(300.0, 300.0);
        scope.draw_arc(brush.clone(), center, 20.0, 0.0, TAU, Stroke::new(4.0));
        scope.draw_arc(brush.clone(), center, 90.0, 0.3, 1.2, Stroke::new(6.0));
        scope.draw_arc(brush.clone(), center, 200.0, 4.0, 2.5, Stroke::new(12.0));
        scope.draw_arc(brush.clone(), center, 250.0, 5.5, 2.0, Stroke::new(3.0));
        scope.draw_annular_sector(brush.clone(), center, 100.0, 140.0, 1.0, 0.4);
        scope.draw_annular_sector(brush.clone(), center, 12.0, 30.0, 2.0, 3.0);
        scope.draw_arc(
            brush.clone(),
            Point::new(100.0, 100.0),
            ARC_BAND_MIN_RADIUS,
            0.0,
            TAU,
            Stroke::new(2.0),
        );
        scope.draw_annular_sector(brush.clone(), center, 10.0, 30.0, 0.7, 0.3);
        scope.draw_arc(brush.clone(), center, 80.0, 5.0, 0.2, Stroke::new(2.0));
        scope.draw_arc(
            brush.clone(),
            center,
            60.0,
            2.0,
            0.15,
            Stroke::new(14.0).with_cap(StrokeCap::Square),
        );
        scope.draw_arc(
            brush,
            center,
            40.0,
            3.0,
            0.5,
            Stroke::new(16.0).with_cap(StrokeCap::Round),
        );
    });
    let banded: Vec<bool> = records.iter().map(ShapeRecord::is_banded).collect();
    assert_eq!(
        banded,
        [
            true, true, true, true, true, true, false, true, true, true, true
        ],
        "the ring at the smallest band radius costs more as a strip than as \
         its quad once its vertices are charged"
    );
    for record in records.iter().filter(|record| record.is_banded()) {
        for scale in [1.0, 2.75] {
            assert_strip_covers_shader(record, scale);
        }
    }
}

#[test]
fn the_analytic_strip_area_equals_its_triangles() {
    let records = recorded_arcs(|scope| {
        let center = Point::new(300.0, 300.0);
        let brush = Brush::solid(Color::WHITE);
        scope.draw_arc(brush.clone(), center, 20.0, 0.0, TAU, Stroke::new(4.0));
        scope.draw_arc(brush.clone(), center, 90.0, 0.3, 1.2, Stroke::new(6.0));
        scope.draw_annular_sector(brush, center, 100.0, 140.0, 1.0, 0.4);
    });
    for record in &records {
        let strip = BandStrip::of(record, Point::new(3.0, 7.0), 1.5, record.band_segments());
        let analytic = strip.area();
        let summed = strip.triangle_area_sum();
        assert!(
            (analytic - summed).abs() <= summed * 1e-4,
            "{analytic} vs {summed} for {record:?}"
        );
    }
}

#[test]
fn the_fill_estimate_counts_strips_for_bands_and_quads_for_the_rest() {
    let mut scope = DrawScopeDefault::new(Size::new(600.0, 600.0));
    scope.draw_arc(
        Brush::solid(Color::WHITE),
        Point::new(300.0, 300.0),
        200.0,
        0.0,
        TAU,
        Stroke::new(4.0),
    );
    scope.draw_rect_at(
        cranpose_ui_graphics::Rect {
            x: 0.0,
            y: 0.0,
            width: 10.0,
            height: 20.0,
        },
        Brush::solid(Color::WHITE),
    );
    let recording = scope.finish();
    let tables = recording.tables();
    let draws = |bands: bool| {
        tables.segments.iter().map(move |segment| {
            (
                segment.start..segment.start + segment.count,
                bands.then(|| band_class_segments(segment.band_class)),
            )
        })
    };
    let banded = ShapeFill::of_draws(tables, Point::default(), 1.0, draws(true));
    let quads = ShapeFill::of_draws(tables, Point::default(), 1.0, draws(false));
    assert_eq!(banded.pixels[0], 200.0);
    assert_eq!(quads.pixels[0], 200.0);
    assert!(banded.pixels[4] < 2.0 * 6284.0 * 8.0);
    assert!(quads.pixels[4] > 150_000.0);
    assert_eq!(banded.total(), banded.pixels[0] + banded.pixels[4]);
    let [segment] = tables.segments.as_slice() else {
        panic!(
            "the ring and the rect share one segment: {:?}",
            tables.segments
        );
    };
    assert_eq!(
        banded.vertices,
        2 * u64::from(strip_vertices(band_class_segments(segment.band_class))),
        "every record of a segment is charged the stride the segment draws at, the rect's \
         pinned vertices included"
    );
    assert_eq!(quads.vertices, 2 * u64::from(QUAD_VERTICES));
}