use crate::canvas::PREVIEW_COLOR;
use crate::canvas::rotate_handle::SNAP_DEG;
use crate::fixture_definition_editor::canvas_geometry::to_scene;
use crate::fixture_definition_editor::edit::Edit;
use crate::fixture_definition_editor::generators;
use crate::fixture_definition_editor::shape::Shape;
use crate::fixture_definition_editor::state::EditorState;
use egui::{
Key, Modifiers, Painter, Pos2, Response, Shape as Drawing, Stroke, emath::TSTransform, vec2,
};
use egui_phosphor_icons::icons;
use gled_model::patching::definition::Pixel;
use strum::{EnumIter, IntoStaticStr};
const CLOSE_PX: f32 = 10.0;
const MAX_CORNER_DOTS: usize = 64;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, EnumIter, IntoStaticStr)]
pub(crate) enum Tool {
#[default]
Select,
Line,
Circle,
Path,
}
impl Tool {
pub(crate) fn label(self) -> (egui::RichText, &'static str) {
let icon = match self {
Self::Select => icons::CURSOR,
Self::Line => icons::LINE_SEGMENT,
Self::Circle => icons::CIRCLE,
Self::Path => icons::PATH,
};
(icon.regular(), self.into())
}
pub(crate) fn key(self) -> Key {
match self {
Self::Select => Key::V,
Self::Line => Key::L,
Self::Circle => Key::C,
Self::Path => Key::P,
}
}
pub(crate) fn hint(self) -> &'static str {
match self {
Self::Select => "",
Self::Line => "Drag from the first LED to the last; shift keeps to 15° steps",
Self::Circle => "Drag from the centre to where the first LED goes",
Self::Path => {
"Click the corners; click the first one again to close the loop, double-click or press Enter to end it"
}
}
}
}
pub(crate) fn snap(metres: [f32; 2]) -> [f32; 2] {
metres.map(|value| (value * 1000.0).round() / 1000.0)
}
pub(crate) fn preview(edit: &Edit<'_>, state: &EditorState) -> Vec<Pixel> {
state
.draw
.pixels(&generators::template(state, edit.value.next_pixel_number()))
}
pub(crate) struct Pointer {
pub(crate) at: Option<[f32; 2]>,
pub(crate) press: Option<[f32; 2]>,
pub(crate) clicked_at: Option<[f32; 2]>,
pub(crate) pixels_per_metre: f32,
pub(crate) modifiers: Modifiers,
}
pub(crate) fn interact(state: &mut EditorState, response: &Response, pointer: &Pointer) {
match state.draw.tool {
Tool::Select => {}
Tool::Line => draw_line(state, response, pointer),
Tool::Circle => draw_circle(state, response, pointer),
Tool::Path => draw_path(state, response, pointer),
}
}
fn draw_line(state: &mut EditorState, response: &Response, pointer: &Pointer) {
if response.drag_started_by(egui::PointerButton::Primary)
&& let Some(press) = pointer.press
{
let from = snap(press);
state.draw.shape = Some(Shape::Line { from, to: from });
}
if (response.dragged_by(egui::PointerButton::Primary)
|| response.drag_stopped_by(egui::PointerButton::Primary))
&& let (Some(at), Some(Shape::Line { from, to })) = (pointer.at, &mut state.draw.shape)
{
let mut end = at;
if pointer.modifiers.shift {
let delta = vec2(at[0] - from[0], at[1] - from[1]);
let step = SNAP_DEG.to_radians();
let angle = (delta.y.atan2(delta.x) / step).round() * step;
let length = delta.length();
end = [
from[0] + length * angle.cos(),
from[1] + length * angle.sin(),
];
}
*to = snap(end);
}
}
fn draw_circle(state: &mut EditorState, response: &Response, pointer: &Pointer) {
if response.drag_started_by(egui::PointerButton::Primary)
&& let Some(press) = pointer.press
{
state.draw.shape = Some(Shape::Circle {
centre: snap(press),
radius: 0.0,
start_deg: 0.0,
});
}
if (response.dragged_by(egui::PointerButton::Primary)
|| response.drag_stopped_by(egui::PointerButton::Primary))
&& let (
Some(at),
Some(Shape::Circle {
centre,
radius,
start_deg,
}),
) = (pointer.at, &mut state.draw.shape)
{
let delta = vec2(at[0] - centre[0], at[1] - centre[1]);
*radius = (delta.length() * 1000.0).round() / 1000.0;
*start_deg = delta.x.atan2(-delta.y).to_degrees();
}
}
fn draw_path(state: &mut EditorState, response: &Response, pointer: &Pointer) {
let Some(at) = pointer.clicked_at.map(snap) else {
return;
};
if response.double_clicked() {
if let Some(Shape::Path {
points, finished, ..
}) = &mut state.draw.shape
{
points.dedup();
*finished = true;
}
return;
}
if !response.clicked_by(egui::PointerButton::Primary) {
return;
}
match &mut state.draw.shape {
Some(Shape::Path {
points,
closed,
finished: finished @ false,
}) => {
let first = points[0];
let distance_px =
vec2(at[0] - first[0], at[1] - first[1]).length() * pointer.pixels_per_metre;
if points.len() >= 3 && distance_px <= CLOSE_PX {
*closed = true;
*finished = true;
} else {
points.push(at);
}
}
_ => {
state.draw.shape = Some(Shape::Path {
points: vec![at],
closed: false,
finished: false,
});
}
}
}
pub(crate) fn paint(
painter: &Painter,
to_screen: TSTransform,
origin: Pos2,
state: &EditorState,
pointer_m: Option<[f32; 2]>,
) {
let stroke = Stroke::new(1.5, PREVIEW_COLOR);
let screen = |metres: [f32; 2]| to_screen * to_scene(origin, [metres[0], metres[1], 0.0]);
match &state.draw.shape {
Some(Shape::Line { from, to }) => {
painter.line_segment([screen(*from), screen(*to)], stroke);
}
Some(Shape::Circle { centre, radius, .. }) => {
let centre_px = screen(*centre);
let radius_px =
to_screen.scaling * radius * gled_model::patching::fixture::UNITS_PER_METRE;
painter.circle_stroke(centre_px, radius_px, stroke);
painter.circle_filled(centre_px, 2.5, PREVIEW_COLOR);
}
Some(Shape::Path {
points,
closed,
finished,
}) => {
let mut corners = points.clone();
if !finished && let Some(at) = pointer_m {
corners.push(snap(at));
}
let mut line: Vec<Pos2> = state
.draw
.course(&corners, *closed)
.into_iter()
.map(screen)
.collect();
if *closed && let Some(first) = line.first().copied() {
line.push(first);
}
painter.add(Drawing::line(line, stroke));
if points.len() <= MAX_CORNER_DOTS {
for point in points {
painter.circle_filled(screen(*point), 3.0, PREVIEW_COLOR);
}
}
}
None => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn positions_snap_to_millimetres() {
assert_eq!(snap([0.01234, -0.0006]), [0.012, -0.001]);
}
}