use crate::canvas::box_scale::{BoxScale, box_handles, scaled_box};
use crate::canvas::box_select::{Band, draw_band};
use crate::canvas::definition_drawing::{MIN_FIXTURE_SCREEN_PX, fixture_footprint};
use crate::canvas::rotate_handle::{RotateDrag, rotation_handle};
use crate::canvas::shapes::Shapes;
use crate::fixture_editor::canvas::{Definitions, definition_of};
use crate::fixture_editor::drag::Drag;
use crate::fixture_editor::state::EditorState;
use egui::emath::TSTransform;
use egui::{Modifiers, PointerButton, Pos2, Rect, Response, Ui};
use gled_model::patching::Patch;
use gled_model::patching::fixture::{FixtureId, LayoutPosition};
use gled_model::patching::layout::LayoutRef;
pub(super) fn box_selection(
state: &mut EditorState,
canvas_response: &Response,
overlay_painter: &mut Shapes,
scene_transform: &TSTransform,
press: Option<Pos2>,
modifiers: Modifiers,
fixture_took_focus: bool,
) {
if canvas_response.drag_started_by(PointerButton::Primary)
&& state.drag.is_none()
&& !fixture_took_focus
&& let Some(press) = press
{
let band = Band::start(press, &mut state.selected_fixtures, modifiers);
state.drag = Some(Drag::Band(band));
}
if let Some(Drag::Band(band)) = &state.drag {
draw_band(overlay_painter, scene_transform, band.rect());
}
}
pub(super) fn covered_by(
patch: &Patch,
drawn: LayoutRef,
rect: Rect,
definitions: &Definitions,
zoom: f32,
) -> Vec<FixtureId> {
let Some(layout) = patch.layout(drawn) else {
return vec![];
};
layout
.members
.iter()
.filter(|(id, position)| {
fixture_footprint(
definition_of(definitions, **id),
position,
zoom,
MIN_FIXTURE_SCREEN_PX,
)
.intersects(rect)
})
.map(|(id, _)| *id)
.collect()
}
fn selection_bbox(
patch: &Patch,
state: &EditorState,
drawn: LayoutRef,
definitions: &Definitions,
zoom: f32,
) -> Option<Rect> {
let layout = patch.layout(drawn)?;
state
.selected_fixtures
.iter()
.filter_map(|id| {
let position = layout.members.get(id)?;
let definition = definition_of(definitions, *id);
Some(fixture_footprint(definition, position, zoom, MIN_FIXTURE_SCREEN_PX).bounds())
})
.reduce(|bounds, footprint| bounds.union(footprint))
}
pub(super) fn draw_bounding_box(
ui: &mut Ui,
patch: &Patch,
state: &mut EditorState,
drawn: LayoutRef,
overlay_painter: &mut Shapes,
scene_transform: &TSTransform,
definitions: &Definitions,
pointer: Option<Pos2>,
modifiers: Modifiers,
) {
let Some(live_box) = selection_bbox(patch, state, drawn, definitions, scene_transform.scaling)
else {
return;
};
let shown_box = match (&state.drag, pointer) {
(Some(Drag::Scale(scale)), Some(pointer)) => {
let (origin, scale_x, scale_y) = scale.factors(pointer, modifiers.alt, modifiers.shift);
scaled_box(scale.start_box, origin, scale_x, scale_y)
}
_ => live_box,
};
let started = box_handles(ui, overlay_painter, scene_transform, shown_box);
if let (Some(handle), Some(pointer)) = (started, pointer) {
state.drag = Some(Drag::Scale(BoxScale {
handle,
start_box: live_box,
start_pointer: pointer,
start: selected_positions(patch, state, drawn),
}));
}
}
pub(super) fn draw_rotation_handle(
ui: &mut Ui,
patch: &Patch,
state: &mut EditorState,
drawn: LayoutRef,
overlay_painter: &mut Shapes,
scene_transform: &TSTransform,
definitions: &Definitions,
pointer: Option<Pos2>,
) {
let Some(live_box) = selection_bbox(patch, state, drawn, definitions, scene_transform.scaling)
else {
return;
};
let shown_box = match &state.drag {
Some(Drag::Rotate(rotation)) => rotation.start_box,
_ => live_box,
};
let handle = rotation_handle(ui, overlay_painter, scene_transform, shown_box);
if handle.drag_started_by(PointerButton::Primary)
&& let Some(pointer) = pointer
{
state.drag = Some(Drag::Rotate(RotateDrag::new(
live_box,
pointer,
selected_positions(patch, state, drawn),
)));
}
}
pub(super) fn selected_positions(
patch: &Patch,
state: &EditorState,
drawn: LayoutRef,
) -> Vec<(FixtureId, LayoutPosition)> {
patch
.layout(drawn)
.map(|layout| {
state
.selected_fixtures
.iter()
.filter_map(|id| layout.members.get(id).map(|position| (*id, *position)))
.collect()
})
.unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
use egui::pos2;
use gled_model::asset::Asset;
use gled_model::asset_id::AssetId;
use gled_model::patching::definition::FixtureDefinition;
use gled_model::patching::definition::{EmittedColor, Pixel};
use gled_model::patching::fixture::Fixture;
fn definitions_for(ids: &[FixtureId]) -> Definitions {
let definition = FixtureDefinition {
size_m: [1.0, 0.05, 0.05],
pixels: vec![Pixel {
name: "pixel-0".to_string(),
model: 0,
position_m: [0.0; 3],
rotation: 0.0,
channels: vec![EmittedColor::Red, EmittedColor::Green, EmittedColor::Blue],
}],
..Default::default()
};
let asset = std::sync::Arc::new(Asset {
id: AssetId::default(),
path: vec!["bar".to_string()],
data: definition,
});
ids.iter().map(|id| (*id, asset.clone())).collect()
}
#[test]
fn covered_by_finds_fixtures_inside_the_box() {
let mut patch = Patch::default();
let inside =
patch.add_fixture(Fixture::new("inside"), LayoutPosition::at(pos2(10.0, 10.0)));
patch.add_fixture(
Fixture::new("outside"),
LayoutPosition::at(pos2(900.0, 900.0)),
);
let definitions = definitions_for(&[inside]);
let hits = covered_by(
&patch,
LayoutRef::Topology,
Rect::from_min_max(pos2(0.0, 0.0), pos2(100.0, 100.0)),
&definitions,
1.0,
);
assert_eq!(hits, vec![inside]);
}
#[test]
fn a_fixture_below_the_legibility_floor_is_band_selected_as_drawn() {
let mut patch = Patch::default();
let bar = patch.add_fixture(Fixture::new("bar"), LayoutPosition::at(pos2(500.0, 500.0)));
let definitions = definitions_for(&[bar]);
let band = Rect::from_min_max(pos2(480.0, 506.0), pos2(520.0, 510.0));
let zoomed_out = covered_by(&patch, LayoutRef::Topology, band, &definitions, 0.45);
let zoomed_in = covered_by(&patch, LayoutRef::Topology, band, &definitions, 8.0);
assert_eq!(zoomed_out, vec![bar], "drawn 11 px deep at 0.45");
assert!(zoomed_in.is_empty(), "drawn at its true size at 8");
}
#[test]
fn selection_bbox_spans_the_selected_fixtures() {
let mut patch = Patch::default();
let mut state = EditorState::default();
let a = patch.add_fixture(Fixture::new("a"), LayoutPosition::at(pos2(0.0, 0.0)));
let b = patch.add_fixture(Fixture::new("b"), LayoutPosition::at(pos2(200.0, 0.0)));
state.selected_fixtures.extend([a, b]);
let definitions = definitions_for(&[a, b]);
let bbox = selection_bbox(&patch, &state, LayoutRef::Topology, &definitions, 1.0)
.expect("two fixtures are selected");
assert_eq!(bbox.min.x, -50.0);
assert_eq!(bbox.max.x, 250.0);
}
}