use std::collections::BTreeMap;
use std::collections::BTreeSet;
use std::io::Cursor;
use image::DynamicImage;
use image::ImageFormat;
use image::Rgba;
use image::RgbaImage;
use super::model::InternalPoint;
use super::model::InternalSegment;
use super::types::SketchVisualizationBounds;
use super::types::SketchVisualizationError;
use super::types::SketchVisualizationPoint;
use crate::front::Freedom;
const CANVAS_WIDTH: u32 = 1024;
const CANVAS_HEIGHT: u32 = 1024;
const CANVAS_PADDING: u32 = 48;
const PRIMARY_LINE_WIDTH: f64 = 3.0;
const POINT_RADIUS: f64 = 4.0;
const CONTACT_POINT_RADIUS: f64 = 5.0;
const FREE_COLOR: Color = Color::rgb(0x3c, 0x73, 0xff);
const CONFLICT_COLOR: Color = Color::rgb(0xff, 0x5e, 0x5b);
const FIXED_COLOR: Color = Color::rgb(0xff, 0xff, 0xff);
const DARK_BACKGROUND: Color = Color::rgb(0x18, 0x1a, 0x1f);
const POINT_OUTLINE_DARK: Color = Color::rgb(0x18, 0x1a, 0x1f);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct Color {
r: u8,
g: u8,
b: u8,
a: u8,
}
impl Color {
const fn rgb(r: u8, g: u8, b: u8) -> Self {
Self { r, g, b, a: 255 }
}
fn to_rgba(self) -> Rgba<u8> {
Rgba([self.r, self.g, self.b, self.a])
}
}
fn dof_color(freedom: Option<Freedom>) -> Color {
match freedom {
Some(Freedom::Conflict) => CONFLICT_COLOR,
Some(Freedom::Fixed) => FIXED_COLOR,
Some(Freedom::Free) | None => FREE_COLOR,
}
}
pub(super) fn render_png(
segments: &BTreeMap<usize, InternalSegment>,
points: &BTreeMap<usize, InternalPoint>,
contact_point_ids: &BTreeSet<usize>,
bounds: SketchVisualizationBounds,
) -> Result<Vec<u8>, SketchVisualizationError> {
let mut image = RgbaImage::from_pixel(CANVAS_WIDTH, CANVAS_HEIGHT, DARK_BACKGROUND.to_rgba());
let transform = Transform::new(bounds);
for segment in segments.values() {
let color = dof_color(segment.freedom);
draw_polyline(&mut image, &segment.polyline, color, segment.construction, &transform);
}
for (point_id, point) in points {
let color = dof_color(Some(point.freedom));
let radius = if contact_point_ids.contains(point_id) {
CONTACT_POINT_RADIUS
} else {
POINT_RADIUS
};
let screen = transform.point(point.position);
draw_filled_circle(&mut image, screen, radius + 1.5, POINT_OUTLINE_DARK);
draw_filled_circle(&mut image, screen, radius, color);
}
let dynamic = DynamicImage::ImageRgba8(image);
let mut cursor = Cursor::new(Vec::new());
dynamic.write_to(&mut cursor, ImageFormat::Png)?;
Ok(cursor.into_inner())
}
#[derive(Debug, Clone, Copy)]
struct Transform {
scale: f64,
offset_x: f64,
offset_y: f64,
}
impl Transform {
fn new(bounds: SketchVisualizationBounds) -> Self {
let content_width = (CANVAS_WIDTH - CANVAS_PADDING * 2) as f64;
let content_height = (CANVAS_HEIGHT - CANVAS_PADDING * 2) as f64;
let world_width = libm::fmax((bounds.max.x - bounds.min.x).abs(), 1.0);
let world_height = libm::fmax((bounds.max.y - bounds.min.y).abs(), 1.0);
let scale = libm::fmin(content_width / world_width, content_height / world_height);
let world_center_x = (bounds.min.x + bounds.max.x) * 0.5;
let world_center_y = (bounds.min.y + bounds.max.y) * 0.5;
let screen_center_x = CANVAS_WIDTH as f64 * 0.5;
let screen_center_y = CANVAS_HEIGHT as f64 * 0.5;
Self {
scale,
offset_x: screen_center_x - world_center_x * scale,
offset_y: screen_center_y + world_center_y * scale,
}
}
fn point(self, point: SketchVisualizationPoint) -> ScreenPoint {
ScreenPoint {
x: point.x * self.scale + self.offset_x,
y: -point.y * self.scale + self.offset_y,
}
}
}
#[derive(Debug, Clone, Copy)]
struct ScreenPoint {
x: f64,
y: f64,
}
fn draw_polyline(
image: &mut RgbaImage,
points: &[SketchVisualizationPoint],
color: Color,
dashed: bool,
transform: &Transform,
) {
for segment in points.windows(2) {
let start = transform.point(segment[0]);
let end = transform.point(segment[1]);
if dashed {
draw_dashed_line(image, start, end, color);
} else {
draw_line(image, start, end, color);
}
}
}
fn draw_dashed_line(image: &mut RgbaImage, start: ScreenPoint, end: ScreenPoint, color: Color) {
let length = screen_distance(start, end);
if length <= f64::EPSILON {
return;
}
let dash = 12.0;
let gap = 8.0;
let step = dash + gap;
let mut cursor = 0.0;
while cursor < length {
let dash_end = libm::fmin(cursor + dash, length);
let from = interpolate_screen(start, end, cursor / length);
let to = interpolate_screen(start, end, dash_end / length);
draw_line(image, from, to, color);
cursor += step;
}
}
fn draw_line(image: &mut RgbaImage, start: ScreenPoint, end: ScreenPoint, color: Color) {
let length = screen_distance(start, end);
if length <= f64::EPSILON {
draw_filled_circle(image, start, PRIMARY_LINE_WIDTH * 0.5, color);
return;
}
let samples = length.ceil() as usize;
for index in 0..=samples {
let t = index as f64 / samples as f64;
draw_filled_circle(
image,
interpolate_screen(start, end, t),
PRIMARY_LINE_WIDTH * 0.5,
color,
);
}
}
fn draw_filled_circle(image: &mut RgbaImage, center: ScreenPoint, radius: f64, color: Color) {
let min_x = (center.x - radius).floor() as i32;
let max_x = (center.x + radius).ceil() as i32;
let min_y = (center.y - radius).floor() as i32;
let max_y = (center.y + radius).ceil() as i32;
let radius_sq = radius * radius;
for y in min_y..=max_y {
for x in min_x..=max_x {
if x < 0 || y < 0 || x >= image.width() as i32 || y >= image.height() as i32 {
continue;
}
let dx = x as f64 + 0.5 - center.x;
let dy = y as f64 + 0.5 - center.y;
if dx * dx + dy * dy <= radius_sq {
image.put_pixel(x as u32, y as u32, color.to_rgba());
}
}
}
}
fn screen_distance(a: ScreenPoint, b: ScreenPoint) -> f64 {
((a.x - b.x).powi(2) + (a.y - b.y).powi(2)).sqrt()
}
fn interpolate_screen(a: ScreenPoint, b: ScreenPoint, t: f64) -> ScreenPoint {
ScreenPoint {
x: a.x + (b.x - a.x) * t,
y: a.y + (b.y - a.y) * t,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tooling::sketch_visualizer::sampling::sample_circle;
#[test]
fn point_colors_are_independent_of_segment_colors() {
let center = SketchVisualizationPoint { x: 0.0, y: 0.0 };
let bounds = SketchVisualizationBounds {
min: SketchVisualizationPoint { x: -12.0, y: -12.0 },
max: SketchVisualizationPoint { x: 12.0, y: 12.0 },
};
for segment_freedom in [Freedom::Free, Freedom::Fixed, Freedom::Conflict] {
for point_freedom in [Freedom::Free, Freedom::Fixed, Freedom::Conflict] {
let segments = BTreeMap::from([(
1,
InternalSegment {
construction: false,
freedom: Some(segment_freedom),
polyline: sample_circle(center, 10.0),
},
)]);
let start = SketchVisualizationPoint { x: 10.0, y: 0.0 };
let points = BTreeMap::from([
(
2,
InternalPoint {
position: center,
freedom: point_freedom,
},
),
(
3,
InternalPoint {
position: start,
freedom: point_freedom,
},
),
]);
let png = render_png(&segments, &points, &BTreeSet::new(), bounds).unwrap();
let image = image::load_from_memory(&png).unwrap().into_rgba8();
for position in [center, start] {
let pixel = Transform::new(bounds).point(position);
assert_eq!(
*image.get_pixel(pixel.x as u32, pixel.y as u32),
dof_color(Some(point_freedom)).to_rgba(),
"point={point_freedom:?}, segment={segment_freedom:?}"
);
}
let rim = Transform::new(bounds).point(SketchVisualizationPoint { x: -10.0, y: 0.0 });
assert_eq!(
*image.get_pixel(rim.x as u32, rim.y as u32),
dof_color(Some(segment_freedom)).to_rgba(),
"the circle stroke must retain its segment constraint color"
);
}
}
}
}