use crate::constraints::Constraint;
use crate::datatypes::inputs::{DatumLineSegment, DatumPoint};
use crate::solver::{Config, Layout};
use std::io;
use std::path::Path;
const ZERO_RESIDUAL_THRESHOLD: f64 = 0.08;
const TURQUOISE: [u8; 3] = [64, 224, 208];
const EXAMPLE_POINT_X: f64 = 3.0;
const EXAMPLE_POINT_Y: f64 = 2.0;
const DISTANCE_EXAMPLE_POINT_X: f64 = 4.5;
const DISTANCE_EXAMPLE_POINT_Y: f64 = 3.0;
const PERP_DISTANCE_EXAMPLE_POINT_X: f64 = -2.0;
const PERP_DISTANCE_EXAMPLE_POINT_Y: f64 = 5.0;
const VERTICAL_HORIZONTAL_EXAMPLE_POINT_X: f64 = 3.0;
const VERTICAL_HORIZONTAL_EXAMPLE_POINT_Y: f64 = 2.0;
const RING_SCALE: f64 = 1.0;
struct Viewport {
x_min: f64,
x_max: f64,
y_min: f64,
y_max: f64,
width: u32,
height: u32,
}
impl Viewport {
fn new(x_min: f64, x_max: f64, y_min: f64, y_max: f64, width: u32, height: u32) -> Self {
Self {
x_min,
x_max,
y_min,
y_max,
width,
height,
}
}
fn pixel_center_to_world(&self, px: u32, py: u32) -> (f64, f64) {
let x = self.x_min + (self.x_max - self.x_min) * (px as f64 + 0.5) / (self.width as f64);
let y = self.y_min + (self.y_max - self.y_min) * (py as f64 + 0.5) / (self.height as f64);
(x, y)
}
fn world_to_pixel(&self, x: f64, y: f64) -> (i32, i32) {
let px = (x - self.x_min) / (self.x_max - self.x_min) * (self.width as f64);
let py = (y - self.y_min) / (self.y_max - self.y_min) * (self.height as f64);
(px.round() as i32, py.round() as i32)
}
}
fn mag_to_pixel(mag: f64) -> image::Rgb<u8> {
if mag < ZERO_RESIDUAL_THRESHOLD {
image::Rgb(TURQUOISE)
} else {
let value = mag * RING_SCALE;
let fractional = value - value.trunc();
let intensity = (255.0 - fractional * 255.0).round() as u8;
image::Rgb([intensity, intensity, intensity])
}
}
fn draw_filled_circle(buf: &mut image::RgbImage, cx: i32, cy: i32, radius_px: i32, color: [u8; 3]) {
let w = buf.width() as i32;
let h = buf.height() as i32;
for dy in -radius_px..=radius_px {
for dx in -radius_px..=radius_px {
if dx * dx + dy * dy <= radius_px * radius_px {
let px = cx + dx;
let py = cy + dy;
if px >= 0 && px < w && py >= 0 && py < h {
buf.put_pixel(px as u32, py as u32, image::Rgb(color));
}
}
}
}
}
fn draw_line_segment(
buf: &mut image::RgbImage,
x0: i32,
y0: i32,
x1: i32,
y1: i32,
color: [u8; 3],
) {
let w = buf.width() as i32;
let h = buf.height() as i32;
let dx = (x1 - x0).abs();
let dy = (y1 - y0).abs();
let steps = (dx.max(dy)).max(1);
for i in 0..=steps {
let t = (i as f64) / (steps as f64);
let px = (x0 as f64 + (x1 - x0) as f64 * t).round() as i32;
let py = (y0 as f64 + (y1 - y0) as f64 * t).round() as i32;
if px >= 0 && px < w && py >= 0 && py < h {
buf.put_pixel(px as u32, py as u32, image::Rgb(color));
}
}
}
fn draw_arrow(
buf: &mut image::RgbImage,
from_px: i32,
from_py: i32,
to_px: i32,
to_py: i32,
color: [u8; 3],
head_size_px: i32,
length_fraction: f64,
) {
let w = buf.width() as i32;
let h = buf.height() as i32;
let dx = to_px - from_px;
let dy = to_py - from_py;
let len = libm::hypot(dx as f64, dy as f64);
if len < 1.0 {
return;
}
let ux = dx as f64 / len;
let uy = dy as f64 / len;
let actual_len = len * length_fraction;
let tip_px = from_px + (ux * actual_len).round() as i32;
let tip_py = from_py + (uy * actual_len).round() as i32;
let steps = (actual_len as i32).max(2);
for i in 0..=steps {
let t = (i as f64) / (steps as f64);
let px = from_px + (ux * actual_len * t).round() as i32;
let py = from_py + (uy * actual_len * t).round() as i32;
if px >= 0 && px < w && py >= 0 && py < h {
buf.put_pixel(px as u32, py as u32, image::Rgb(color));
}
}
let back_px = tip_px - (ux * (head_size_px as f64)).round() as i32;
let back_py = tip_py - (uy * (head_size_px as f64)).round() as i32;
let perp_x = (-uy * (head_size_px as f64 * 0.6)).round() as i32;
let perp_y = (ux * (head_size_px as f64 * 0.6)).round() as i32;
let c1x = back_px + perp_x;
let c1y = back_py + perp_y;
let c2x = back_px - perp_x;
let c2y = back_py - perp_y;
draw_line_segment(buf, tip_px, tip_py, c1x, c1y, color);
draw_line_segment(buf, tip_px, tip_py, c2x, c2y, color);
draw_line_segment(buf, c1x, c1y, c2x, c2y, color);
}
fn render_residual_field<F>(viewport: &Viewport, mut sample: F) -> image::RgbImage
where
F: FnMut(f64, f64) -> f64,
{
let mut buf = image::RgbImage::new(viewport.width, viewport.height);
for py in 0..viewport.height {
for px in 0..viewport.width {
let (x, y) = viewport.pixel_center_to_world(px, py);
let mag = sample(x, y);
buf.put_pixel(px, py, mag_to_pixel(mag));
}
}
buf
}
fn draw_solver_overlay(
buf: &mut image::RgbImage,
viewport: &Viewport,
example_x: f64,
example_y: f64,
solution_x: f64,
solution_y: f64,
) {
let (ex_px, ex_py) = viewport.world_to_pixel(example_x, example_y);
let (sol_px, sol_py) = viewport.world_to_pixel(solution_x, solution_y);
draw_arrow(buf, ex_px, ex_py, sol_px, sol_py, [200, 0, 0], 6, 0.5);
draw_filled_circle(buf, ex_px, ex_py, 5, [255, 0, 0]);
draw_filled_circle(buf, sol_px, sol_py, 5, [0, 180, 0]);
}
pub fn render_points_coincident_residual_to_image(
fixed_x: f64,
fixed_y: f64,
x_min: f64,
x_max: f64,
y_min: f64,
y_max: f64,
width: u32,
height: u32,
) -> image::RgbImage {
let viewport = Viewport::new(x_min, x_max, y_min, y_max, width, height);
let p0 = DatumPoint::new_xy(0, 1);
let p1 = DatumPoint::new_xy(2, 3);
let constraint = Constraint::PointsCoincident(p0, p1);
let layout = Layout::new(&[0, 1, 2, 3], &[&constraint], Config::default());
let mut assignments = [0.0_f64; 4];
assignments[2] = fixed_x;
assignments[3] = fixed_y;
let mut buf = render_residual_field(&viewport, |x, y| {
assignments[0] = x;
assignments[1] = y;
let mut r0 = 0.0_f64;
let mut r1 = 0.0_f64;
let mut r2 = 0.0_f64;
let mut degenerate = false;
constraint.residual(
&layout,
&assignments,
&mut r0,
&mut r1,
&mut r2,
&mut degenerate,
);
(r0 * r0 + r1 * r1).sqrt()
});
draw_solver_overlay(
&mut buf,
&viewport,
EXAMPLE_POINT_X,
EXAMPLE_POINT_Y,
fixed_x,
fixed_y,
);
buf
}
pub fn render_distance_residual_to_image(
fixed_x: f64,
fixed_y: f64,
target_distance: f64,
x_min: f64,
x_max: f64,
y_min: f64,
y_max: f64,
width: u32,
height: u32,
) -> image::RgbImage {
let viewport = Viewport::new(x_min, x_max, y_min, y_max, width, height);
let p0 = DatumPoint::new_xy(0, 1);
let p1 = DatumPoint::new_xy(2, 3);
let constraint = Constraint::Distance(p0, p1, target_distance);
let layout = Layout::new(&[0, 1, 2, 3], &[&constraint], Config::default());
let mut assignments = [0.0_f64; 4];
assignments[2] = fixed_x;
assignments[3] = fixed_y;
let mut buf = render_residual_field(&viewport, |x, y| {
assignments[0] = x;
assignments[1] = y;
let mut r0 = 0.0_f64;
let mut r1 = 0.0_f64;
let mut r2 = 0.0_f64;
let mut degenerate = false;
constraint.residual(
&layout,
&assignments,
&mut r0,
&mut r1,
&mut r2,
&mut degenerate,
);
r0.abs()
});
let ex_x = DISTANCE_EXAMPLE_POINT_X;
let ex_y = DISTANCE_EXAMPLE_POINT_Y;
let dx = ex_x - fixed_x;
let dy = ex_y - fixed_y;
let dist_to_ex = libm::hypot(dx, dy);
let (sol_x, sol_y) = if dist_to_ex > 1e-10 {
let ux = dx / dist_to_ex;
let uy = dy / dist_to_ex;
(
fixed_x + ux * target_distance,
fixed_y + uy * target_distance,
)
} else {
(fixed_x + target_distance, fixed_y)
};
draw_solver_overlay(&mut buf, &viewport, ex_x, ex_y, sol_x, sol_y);
buf
}
fn line_equation(px: f64, py: f64, qx: f64, qy: f64) -> (f64, f64, f64) {
let a = py - qy;
let b = qx - px;
let c = px * qy - qx * py;
(a, b, c)
}
pub fn render_point_line_distance_residual_to_image(
line_p0_x: f64,
line_p0_y: f64,
line_p1_x: f64,
line_p1_y: f64,
target_distance: f64,
x_min: f64,
x_max: f64,
y_min: f64,
y_max: f64,
width: u32,
height: u32,
) -> image::RgbImage {
let viewport = Viewport::new(x_min, x_max, y_min, y_max, width, height);
let point = DatumPoint::new_xy(0, 1);
let line = DatumLineSegment::new(DatumPoint::new_xy(2, 3), DatumPoint::new_xy(4, 5));
let constraint = Constraint::PointLineDistance(point, line, target_distance);
let layout = Layout::new(&[0, 1, 2, 3, 4, 5], &[&constraint], Config::default());
let mut assignments = [0.0_f64; 6];
assignments[2] = line_p0_x;
assignments[3] = line_p0_y;
assignments[4] = line_p1_x;
assignments[5] = line_p1_y;
let (a, b, c) = line_equation(line_p0_x, line_p0_y, line_p1_x, line_p1_y);
let denom = libm::hypot(a, b);
let denom = if denom > 1e-10 { denom } else { 1.0 };
let mut buf = render_residual_field(&viewport, |x, y| {
assignments[0] = x;
assignments[1] = y;
let mut r0 = 0.0_f64;
let mut r1 = 0.0_f64;
let mut r2 = 0.0_f64;
let mut degenerate = false;
constraint.residual(
&layout,
&assignments,
&mut r0,
&mut r1,
&mut r2,
&mut degenerate,
);
r0.abs()
});
let ex_x = PERP_DISTANCE_EXAMPLE_POINT_X;
let ex_y = PERP_DISTANCE_EXAMPLE_POINT_Y;
let actual = (a * ex_x + b * ex_y + c) / denom;
let sol_x = ex_x + a / denom * (target_distance - actual);
let sol_y = ex_y + b / denom * (target_distance - actual);
draw_solver_overlay(&mut buf, &viewport, ex_x, ex_y, sol_x, sol_y);
buf
}
pub fn render_vertical_residual_to_image(
fixed_x: f64,
fixed_y: f64,
x_min: f64,
x_max: f64,
y_min: f64,
y_max: f64,
width: u32,
height: u32,
) -> image::RgbImage {
let viewport = Viewport::new(x_min, x_max, y_min, y_max, width, height);
let line = DatumLineSegment::new(DatumPoint::new_xy(0, 1), DatumPoint::new_xy(2, 3));
let constraint = Constraint::Vertical(line);
let layout = Layout::new(&[0, 1, 2, 3], &[&constraint], Config::default());
let mut assignments = [0.0_f64; 4];
assignments[2] = fixed_x;
assignments[3] = fixed_y;
let mut buf = render_residual_field(&viewport, |x, y| {
assignments[0] = x;
assignments[1] = y;
let mut r0 = 0.0_f64;
let mut r1 = 0.0_f64;
let mut r2 = 0.0_f64;
let mut degenerate = false;
constraint.residual(
&layout,
&assignments,
&mut r0,
&mut r1,
&mut r2,
&mut degenerate,
);
r0.abs()
});
let ex_x = VERTICAL_HORIZONTAL_EXAMPLE_POINT_X;
let ex_y = VERTICAL_HORIZONTAL_EXAMPLE_POINT_Y;
let sol_x = fixed_x;
let sol_y = ex_y;
draw_solver_overlay(&mut buf, &viewport, ex_x, ex_y, sol_x, sol_y);
buf
}
pub fn render_horizontal_residual_to_image(
fixed_x: f64,
fixed_y: f64,
x_min: f64,
x_max: f64,
y_min: f64,
y_max: f64,
width: u32,
height: u32,
) -> image::RgbImage {
let viewport = Viewport::new(x_min, x_max, y_min, y_max, width, height);
let line = DatumLineSegment::new(DatumPoint::new_xy(0, 1), DatumPoint::new_xy(2, 3));
let constraint = Constraint::Horizontal(line);
let layout = Layout::new(&[0, 1, 2, 3], &[&constraint], Config::default());
let mut assignments = [0.0_f64; 4];
assignments[2] = fixed_x;
assignments[3] = fixed_y;
let mut buf = render_residual_field(&viewport, |x, y| {
assignments[0] = x;
assignments[1] = y;
let mut r0 = 0.0_f64;
let mut r1 = 0.0_f64;
let mut r2 = 0.0_f64;
let mut degenerate = false;
constraint.residual(
&layout,
&assignments,
&mut r0,
&mut r1,
&mut r2,
&mut degenerate,
);
r0.abs()
});
let ex_x = VERTICAL_HORIZONTAL_EXAMPLE_POINT_X;
let ex_y = VERTICAL_HORIZONTAL_EXAMPLE_POINT_Y;
let sol_x = ex_x;
let sol_y = fixed_y;
draw_solver_overlay(&mut buf, &viewport, ex_x, ex_y, sol_x, sol_y);
buf
}
pub fn render_points_coincident_residual(
path: &Path,
fixed_x: f64,
fixed_y: f64,
x_min: f64,
x_max: f64,
y_min: f64,
y_max: f64,
width: u32,
height: u32,
) -> Result<(), io::Error> {
let buf = render_points_coincident_residual_to_image(
fixed_x, fixed_y, x_min, x_max, y_min, y_max, width, height,
);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
}
buf.save(path)
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))
}
#[cfg(all(test, feature = "residual-viz"))]
mod tests {
use super::*;
use std::path::PathBuf;
const POINTS_COINCIDENT_BASELINE: &str = concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/residual_viz_baselines/points_coincident.png"
);
const DISTANCE_BASELINE: &str = concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/residual_viz_baselines/distance.png"
);
const POINT_LINE_DISTANCE_BASELINE: &str = concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/residual_viz_baselines/point_line_distance.png"
);
const VERTICAL_BASELINE: &str = concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/residual_viz_baselines/vertical.png"
);
const HORIZONTAL_BASELINE: &str = concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/residual_viz_baselines/horizontal.png"
);
#[test]
fn points_coincident_residual_matches_baseline() {
let actual =
render_points_coincident_residual_to_image(0.0, 0.0, -5.0, 5.0, -5.0, 5.0, 256, 256);
let dynamic = image::DynamicImage::ImageRgb8(actual);
twenty_twenty::assert_image(POINTS_COINCIDENT_BASELINE, &dynamic, 0.99);
}
#[test]
fn points_coincident_residual_renders_to_file() {
let out_dir: PathBuf = std::env::var("CARGO_TARGET_DIR")
.unwrap_or_else(|_| "target".into())
.into();
let path = out_dir.join("residual_viz_points_coincident.png");
let result =
render_points_coincident_residual(&path, 0.0, 0.0, -5.0, 5.0, -5.0, 5.0, 256, 256);
result.expect("residual viz should write PNG");
}
#[test]
fn distance_residual_matches_baseline() {
let actual =
render_distance_residual_to_image(0.0, 0.0, 3.0, -5.0, 5.0, -5.0, 5.0, 256, 256);
let dynamic = image::DynamicImage::ImageRgb8(actual);
twenty_twenty::assert_image(DISTANCE_BASELINE, &dynamic, 0.99);
}
#[test]
fn point_line_distance_residual_matches_baseline() {
let actual = render_point_line_distance_residual_to_image(
-4.0, -2.0, 4.0, 2.0, 2.0, -5.0, 5.0, -5.0, 5.0, 256, 256,
);
let dynamic = image::DynamicImage::ImageRgb8(actual);
twenty_twenty::assert_image(POINT_LINE_DISTANCE_BASELINE, &dynamic, 0.99);
}
#[test]
fn vertical_residual_matches_baseline() {
let actual = render_vertical_residual_to_image(0.0, 0.0, -5.0, 5.0, -5.0, 5.0, 256, 256);
let dynamic = image::DynamicImage::ImageRgb8(actual);
twenty_twenty::assert_image(VERTICAL_BASELINE, &dynamic, 0.99);
}
#[test]
fn horizontal_residual_matches_baseline() {
let actual = render_horizontal_residual_to_image(0.0, 0.0, -5.0, 5.0, -5.0, 5.0, 256, 256);
let dynamic = image::DynamicImage::ImageRgb8(actual);
twenty_twenty::assert_image(HORIZONTAL_BASELINE, &dynamic, 0.99);
}
}