use crate::fixture_definition_editor::draw_tools::Tool;
use gled_model::patching::definition::Pixel;
use gled_model::patching::definition::generators::{
PathSpec, PixelTemplate, RingSpec, path, ring, smooth,
};
const SMOOTH_STEPS: usize = 16;
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum Shape {
Line {
from: [f32; 2],
to: [f32; 2],
},
Circle {
centre: [f32; 2],
radius: f32,
start_deg: f32,
},
Path {
points: Vec<[f32; 2]>,
closed: bool,
finished: bool,
},
}
#[derive(Debug, Clone)]
pub(crate) struct DrawState {
pub(crate) tool: Tool,
pub(crate) shape: Option<Shape>,
pub(crate) count: usize,
pub(crate) inset: bool,
pub(crate) smooth: bool,
}
impl Default for DrawState {
fn default() -> Self {
Self {
tool: Tool::Select,
shape: None,
count: 10,
inset: false,
smooth: false,
}
}
}
impl DrawState {
pub(crate) fn complete(&self) -> bool {
match &self.shape {
Some(Shape::Line { from, to }) => from != to,
Some(Shape::Circle { radius, .. }) => *radius > 0.0,
Some(Shape::Path { points, .. }) => points.len() >= 2,
None => false,
}
}
fn length(&self) -> Option<f32> {
if !self.complete() {
return None;
}
match &self.shape {
Some(Shape::Line { from, to }) => Some((to[0] - from[0]).hypot(to[1] - from[1])),
Some(Shape::Circle { radius, .. }) => Some(std::f32::consts::TAU * radius),
Some(Shape::Path { points, closed, .. }) => {
let mut course = self.course(points, *closed);
if *closed && let Some(first) = course.first().copied() {
course.push(first);
}
Some(
course
.windows(2)
.map(|pair| (pair[1][0] - pair[0][0]).hypot(pair[1][1] - pair[0][1]))
.sum(),
)
}
None => None,
}
}
fn ends_on_points(&self) -> bool {
match &self.shape {
Some(Shape::Line { .. }) => !self.inset,
Some(Shape::Path { closed, .. }) => !closed && !self.inset,
_ => false,
}
}
pub(crate) fn pitch(&self) -> Option<f32> {
let length = self.length()?;
let gaps = if self.ends_on_points() {
self.count.saturating_sub(1)
} else {
self.count
};
(gaps > 0).then(|| length / gaps as f32)
}
pub(crate) fn count_for_pitch(&self, pitch: f32) -> usize {
let Some(length) = self.length().filter(|_| pitch > 0.0) else {
return self.count;
};
let gaps = ((length / pitch).round() as usize).max(1);
if self.ends_on_points() {
gaps + 1
} else {
gaps
}
}
pub(crate) fn course(&self, points: &[[f32; 2]], closed: bool) -> Vec<[f32; 2]> {
if self.smooth {
smooth(points, closed, SMOOTH_STEPS)
} else {
points.to_vec()
}
}
pub(crate) fn pixels(&self, template: &PixelTemplate) -> Vec<Pixel> {
if !self.complete() {
return vec![];
}
match &self.shape {
Some(Shape::Line { from, to }) => path(
template,
&PathSpec {
points_m: vec![*from, *to],
closed: false,
count: self.count,
inset_ends: self.inset,
},
),
Some(Shape::Circle {
centre,
radius,
start_deg,
}) => ring(
template,
&RingSpec {
centre_m: *centre,
radius_m: *radius,
count: self.count,
start_deg: *start_deg,
clockwise: true,
along_ring: true,
},
),
Some(Shape::Path { points, closed, .. }) => path(
template,
&PathSpec {
points_m: self.course(points, *closed),
closed: *closed,
count: self.count,
inset_ends: self.inset && !closed,
},
),
None => vec![],
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_smooth_path_bends_through_its_corners() {
let corners = vec![[0.0, 0.0], [0.1, 0.1], [0.2, 0.0]];
let mut draw = DrawState {
shape: Some(Shape::Path {
points: corners,
closed: false,
finished: true,
}),
count: 5,
..Default::default()
};
let straight = draw.pixels(&template());
draw.smooth = true;
let curved = draw.pixels(&template());
assert!(
(curved[2].position_m[1] - 0.1).abs() < 0.005,
"{:?}",
curved[2]
);
assert!(curved[1].position_m[1] > straight[1].position_m[1] + 0.005);
}
fn template() -> PixelTemplate {
PixelTemplate {
prefix: "p".to_string(),
first_number: 0,
model: 0,
order: Default::default(),
}
}
#[test]
fn a_line_carries_its_count_from_end_to_end() {
let draw = DrawState {
shape: Some(Shape::Line {
from: [0.0, 0.0],
to: [0.9, 0.0],
}),
count: 10,
..Default::default()
};
let pixels = draw.pixels(&template());
assert_eq!(pixels.len(), 10);
assert!((pixels[9].position_m[0] - 0.9).abs() < 1e-5);
}
#[test]
fn a_circle_starts_where_it_was_dragged_to() {
let draw = DrawState {
shape: Some(Shape::Circle {
centre: [0.0, 0.0],
radius: 0.1,
start_deg: 90.0,
}),
count: 4,
..Default::default()
};
let pixels = draw.pixels(&template());
assert!((pixels[0].position_m[0] - 0.1).abs() < 1e-5);
assert!(pixels[0].position_m[1].abs() < 1e-5);
}
#[test]
fn a_path_with_one_corner_carries_nothing_yet() {
let draw = DrawState {
shape: Some(Shape::Path {
points: vec![[0.0, 0.0]],
closed: false,
finished: false,
}),
..Default::default()
};
assert!(draw.pixels(&template()).is_empty());
}
#[test]
fn the_pitch_follows_the_count_and_back() {
let mut draw = DrawState {
shape: Some(Shape::Line {
from: [0.0, 0.0],
to: [1.0, 0.0],
}),
count: 11,
..Default::default()
};
assert!((draw.pitch().expect("a complete line") - 0.1).abs() < 1e-6);
assert_eq!(draw.count_for_pitch(1.0 / 60.0), 61);
draw.inset = true;
assert_eq!(draw.count_for_pitch(0.1), 10);
}
}