use blinc_core::draw::{Path, Stroke};
use blinc_core::layer::{Color, CornerRadius, Point, Rect};
use blinc_core::{Brush, DrawContext};
use crate::viewport::CanvasViewport;
#[derive(Clone, Debug)]
pub enum CanvasBackground {
None,
Dots(PatternConfig),
Grid(PatternConfig),
Crosshatch(PatternConfig),
}
#[derive(Clone, Debug)]
pub struct PatternConfig {
pub spacing: f32,
pub color: Color,
pub size: f32,
pub zoom_adaptive: Option<ZoomAdaptive>,
}
#[derive(Clone, Debug)]
pub struct ZoomAdaptive {
pub zoom_threshold: f32,
pub coarse_factor: u32,
}
impl CanvasBackground {
pub fn dots() -> Self {
Self::Dots(PatternConfig {
spacing: 50.0,
color: Color::rgba(0.25, 0.25, 0.3, 0.5),
size: 2.0,
zoom_adaptive: None,
})
}
pub fn grid() -> Self {
Self::Grid(PatternConfig {
spacing: 50.0,
color: Color::rgba(0.25, 0.25, 0.3, 0.3),
size: 1.0,
zoom_adaptive: None,
})
}
pub fn crosshatch() -> Self {
Self::Crosshatch(PatternConfig {
spacing: 40.0,
color: Color::rgba(0.25, 0.25, 0.3, 0.25),
size: 1.0,
zoom_adaptive: None,
})
}
pub fn with_spacing(mut self, spacing: f32) -> Self {
if let Some(c) = self.config_mut() {
c.spacing = spacing;
}
self
}
pub fn with_color(mut self, color: Color) -> Self {
if let Some(c) = self.config_mut() {
c.color = color;
}
self
}
pub fn with_size(mut self, size: f32) -> Self {
if let Some(c) = self.config_mut() {
c.size = size;
}
self
}
pub fn with_zoom_adaptive(mut self, threshold: f32, coarse_factor: u32) -> Self {
if let Some(c) = self.config_mut() {
c.zoom_adaptive = Some(ZoomAdaptive {
zoom_threshold: threshold,
coarse_factor: coarse_factor.max(1),
});
}
self
}
fn config_mut(&mut self) -> Option<&mut PatternConfig> {
match self {
Self::None => None,
Self::Dots(c) | Self::Grid(c) | Self::Crosshatch(c) => Some(c),
}
}
}
const MAX_PRIMITIVES: usize = 10_000;
impl CanvasBackground {
pub fn draw(
&self,
ctx: &mut dyn DrawContext,
viewport: &CanvasViewport,
screen_w: f32,
screen_h: f32,
) {
match self {
Self::None => {}
Self::Dots(config) => draw_dots(ctx, viewport, screen_w, screen_h, config),
Self::Grid(config) => draw_grid_lines(ctx, viewport, screen_w, screen_h, config),
Self::Crosshatch(config) => draw_crosshatch(ctx, viewport, screen_w, screen_h, config),
}
}
}
fn visible_content_rect(vp: &CanvasViewport, screen_w: f32, screen_h: f32) -> (f32, f32, f32, f32) {
let tl = vp.screen_to_content(Point::new(0.0, 0.0));
let br = vp.screen_to_content(Point::new(screen_w, screen_h));
(tl.x, tl.y, br.x, br.y)
}
fn cell_range(content_min: f32, content_max: f32, spacing: f32, coarse: u32) -> (f32, f32, f32) {
let step = spacing * coarse as f32;
let first = (content_min / step).floor() * step;
let last = (content_max / step).ceil() * step;
(first, last, step)
}
fn effective_coarse(config: &PatternConfig, zoom: f32) -> u32 {
match &config.zoom_adaptive {
Some(za) if zoom < za.zoom_threshold => za.coarse_factor,
_ => 1,
}
}
fn draw_dots(
ctx: &mut dyn DrawContext,
viewport: &CanvasViewport,
screen_w: f32,
screen_h: f32,
config: &PatternConfig,
) {
let (left, top, right, bottom) = visible_content_rect(viewport, screen_w, screen_h);
let coarse = effective_coarse(config, viewport.zoom);
let (first_x, last_x, step) = cell_range(left, right, config.spacing, coarse);
let (first_y, last_y, _) = cell_range(top, bottom, config.spacing, coarse);
let brush = Brush::Solid(config.color);
let half = config.size / 2.0;
let radius = CornerRadius::uniform(half);
let mut count = 0;
let mut x = first_x;
while x <= last_x {
let mut y = first_y;
while y <= last_y {
if count >= MAX_PRIMITIVES {
return;
}
ctx.fill_rect(
Rect::new(x - half, y - half, config.size, config.size),
radius,
brush.clone(),
);
count += 1;
y += step;
}
x += step;
}
}
fn draw_grid_lines(
ctx: &mut dyn DrawContext,
viewport: &CanvasViewport,
screen_w: f32,
screen_h: f32,
config: &PatternConfig,
) {
let (left, top, right, bottom) = visible_content_rect(viewport, screen_w, screen_h);
let coarse = effective_coarse(config, viewport.zoom);
let (first_x, last_x, step) = cell_range(left, right, config.spacing, coarse);
let (first_y, last_y, _) = cell_range(top, bottom, config.spacing, coarse);
let brush = Brush::Solid(config.color);
let lw = config.size;
let half = lw / 2.0;
let height = bottom - top;
let width = right - left;
let mut x = first_x;
while x <= last_x {
ctx.fill_rect(
Rect::new(x - half, top, lw, height),
CornerRadius::uniform(0.0),
brush.clone(),
);
x += step;
}
let mut y = first_y;
while y <= last_y {
ctx.fill_rect(
Rect::new(left, y - half, width, lw),
CornerRadius::uniform(0.0),
brush.clone(),
);
y += step;
}
}
fn draw_crosshatch(
ctx: &mut dyn DrawContext,
viewport: &CanvasViewport,
screen_w: f32,
screen_h: f32,
config: &PatternConfig,
) {
let (left, top, right, bottom) = visible_content_rect(viewport, screen_w, screen_h);
let coarse = effective_coarse(config, viewport.zoom);
let step = config.spacing * coarse as f32;
let brush = Brush::Solid(config.color);
let stroke = Stroke::new(config.size);
let mut count = 0;
let d_min = ((left - bottom) / step).floor() * step;
let d_max = ((right - top) / step).ceil() * step;
let mut d = d_min;
while d <= d_max {
if count >= MAX_PRIMITIVES {
return;
}
let x0 = left.max(d + top);
let y0 = x0 - d;
let x1 = right.min(d + bottom);
let y1 = x1 - d;
if x0 < x1 {
let path = Path::new().move_to(x0, y0).line_to(x1, y1);
ctx.stroke_path(&path, &stroke, brush.clone());
count += 1;
}
d += step;
}
let d_min = ((left + top) / step).floor() * step;
let d_max = ((right + bottom) / step).ceil() * step;
let mut d = d_min;
while d <= d_max {
if count >= MAX_PRIMITIVES {
return;
}
let x0 = left.max(d - bottom);
let y0 = -x0 + d;
let x1 = right.min(d - top);
let y1 = -x1 + d;
if x0 < x1 {
let path = Path::new().move_to(x0, y0).line_to(x1, y1);
ctx.stroke_path(&path, &stroke, brush.clone());
count += 1;
}
d += step;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_visible_content_rect_identity() {
let vp = CanvasViewport::new(); let (l, t, r, b) = visible_content_rect(&vp, 800.0, 600.0);
assert!((l - 0.0).abs() < 1e-3);
assert!((t - 0.0).abs() < 1e-3);
assert!((r - 800.0).abs() < 1e-3);
assert!((b - 600.0).abs() < 1e-3);
}
#[test]
fn test_visible_content_rect_zoomed() {
let mut vp = CanvasViewport::new();
vp.zoom = 2.0; let (l, t, r, b) = visible_content_rect(&vp, 800.0, 600.0);
assert!((r - l - 400.0).abs() < 1e-2);
assert!((b - t - 300.0).abs() < 1e-2);
}
#[test]
fn test_visible_content_rect_panned() {
let mut vp = CanvasViewport::new();
vp.pan_x = 100.0; vp.pan_y = -50.0;
let (l, t, _r, _b) = visible_content_rect(&vp, 800.0, 600.0);
assert!((l - (-100.0)).abs() < 1e-2);
assert!((t - 50.0).abs() < 1e-2);
}
#[test]
fn test_cell_range_positive() {
let (first, last, step) = cell_range(75.0, 250.0, 50.0, 1);
assert_eq!(step, 50.0);
assert_eq!(first, 50.0); assert_eq!(last, 250.0); }
#[test]
fn test_cell_range_negative() {
let (first, last, step) = cell_range(-120.0, -30.0, 50.0, 1);
assert_eq!(step, 50.0);
assert_eq!(first, -150.0); assert_eq!(last, 0.0); }
#[test]
fn test_cell_range_coarse() {
let (first, last, step) = cell_range(0.0, 500.0, 50.0, 5);
assert_eq!(step, 250.0); assert_eq!(first, 0.0);
assert_eq!(last, 500.0);
}
#[test]
fn test_effective_coarse_below_threshold() {
let config = PatternConfig {
spacing: 50.0,
color: Color::rgba(1.0, 1.0, 1.0, 1.0),
size: 2.0,
zoom_adaptive: Some(ZoomAdaptive {
zoom_threshold: 0.5,
coarse_factor: 5,
}),
};
assert_eq!(effective_coarse(&config, 0.3), 5);
assert_eq!(effective_coarse(&config, 0.5), 1); assert_eq!(effective_coarse(&config, 1.0), 1);
}
#[test]
fn test_effective_coarse_no_adaptive() {
let config = PatternConfig {
spacing: 50.0,
color: Color::rgba(1.0, 1.0, 1.0, 1.0),
size: 2.0,
zoom_adaptive: None,
};
assert_eq!(effective_coarse(&config, 0.1), 1);
}
#[test]
fn test_builder_chaining() {
let bg = CanvasBackground::dots()
.with_spacing(30.0)
.with_color(Color::rgba(1.0, 0.0, 0.0, 1.0))
.with_size(4.0)
.with_zoom_adaptive(0.3, 5);
match bg {
CanvasBackground::Dots(c) => {
assert_eq!(c.spacing, 30.0);
assert_eq!(c.size, 4.0);
assert!(c.zoom_adaptive.is_some());
let za = c.zoom_adaptive.unwrap();
assert_eq!(za.zoom_threshold, 0.3);
assert_eq!(za.coarse_factor, 5);
}
_ => panic!("Expected Dots"),
}
}
#[test]
fn test_none_builder_noop() {
let bg = CanvasBackground::None.with_spacing(30.0).with_size(5.0);
assert!(matches!(bg, CanvasBackground::None));
}
}